@bigcommerce/catalyst 1.0.0-alpha.6 → 1.1.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
@@ -1,24 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli/lib/auth-errors.ts
4
- var UnauthorizedError = class extends Error {
5
- constructor() {
6
- super(
7
- "Your access token is invalid or has expired.\nRun `catalyst auth login` to re-authenticate."
8
- );
9
- this.name = "UnauthorizedError";
3
+ // src/cli/lib/errors.ts
4
+ var UserActionableError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "UserActionableError";
10
8
  }
11
9
  };
12
- function assertAuthorized(response) {
13
- if (response.status === 401) {
14
- throw new UnauthorizedError();
15
- }
16
- }
17
10
 
18
11
  // src/cli/lib/logger.ts
19
12
  import { createConsola } from "consola";
20
13
  var consola = createConsola({
21
- level: "3" ? parseInt("3", 10) : 3
14
+ level: "3" ? parseInt("3", 10) : 3,
15
+ // Drop the per-line timestamp. It adds noise for a short-lived CLI and, on
16
+ // long lines, consola can't right-align it so it falls back to an inline
17
+ // `[time]` prefix that breaks copy-pasteable command suggestions.
18
+ formatOptions: { date: false }
22
19
  });
23
20
 
24
21
  // src/cli/lib/telemetry.ts
@@ -28,8 +25,13 @@ import { randomBytes, randomUUID } from "crypto";
28
25
  // package.json
29
26
  var package_default = {
30
27
  name: "@bigcommerce/catalyst",
31
- version: "1.0.0-alpha.6",
28
+ version: "1.1.0",
32
29
  type: "module",
30
+ repository: {
31
+ type: "git",
32
+ url: "https://github.com/bigcommerce/catalyst",
33
+ directory: "packages/catalyst"
34
+ },
33
35
  bin: {
34
36
  catalyst: "dist/cli.js"
35
37
  },
@@ -64,7 +66,9 @@ var package_default = {
64
66
  "lodash.kebabcase": "^4.1.1",
65
67
  nypm: "^0.5.4",
66
68
  open: "^10.1.0",
69
+ semver: "^7.7.4",
67
70
  "std-env": "^3.9.0",
71
+ tar: "^7.5.7",
68
72
  "yocto-spinner": "^1.0.0",
69
73
  zod: "^4.0.5"
70
74
  },
@@ -76,6 +80,7 @@ var package_default = {
76
80
  "@types/fs-extra": "^11.0.4",
77
81
  "@types/lodash.kebabcase": "^4.1.9",
78
82
  "@types/node": "^22.15.30",
83
+ "@types/semver": "^7.7.0",
79
84
  "@vitest/coverage-v8": "^3.2.4",
80
85
  "@vitest/ui": "^3.2.4",
81
86
  eslint: "^8.57.1",
@@ -128,7 +133,7 @@ var Telemetry = class {
128
133
  this.correlationId = randomUUID();
129
134
  this.startTime = Date.now();
130
135
  this.analytics = new Analytics({
131
- writeKey: "not-a-valid-segment-write-key"
136
+ writeKey: "OCkxLj3KTc1N3ebEbnFBqbhxfE3tVb3Z"
132
137
  });
133
138
  }
134
139
  durationMs() {
@@ -195,26 +200,65 @@ function getTelemetry() {
195
200
  }
196
201
 
197
202
  // 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 colorize9 } from "consola/utils";
201
- import { config } from "dotenv";
202
- import { existsSync as existsSync5 } from "fs";
203
- import { resolve } from "path";
203
+ import { Command as Command15 } from "commander";
204
+ import { colorize as colorize13 } from "consola/utils";
204
205
 
205
206
  // src/cli/commands/auth.ts
206
- import { Command, Option as Option2 } from "commander";
207
- import { z as z4 } from "zod";
207
+ import { Command } from "commander";
208
+ import { z as z5 } from "zod";
209
+
210
+ // src/cli/lib/auth-errors.ts
211
+ var UnauthorizedError = class extends UserActionableError {
212
+ constructor() {
213
+ super(
214
+ "Your access token is invalid or has expired.\nRun `catalyst auth login` to re-authenticate."
215
+ );
216
+ this.name = "UnauthorizedError";
217
+ }
218
+ };
219
+ function assertAuthorized(response) {
220
+ if (response.status === 401) {
221
+ throw new UnauthorizedError();
222
+ }
223
+ }
208
224
 
209
225
  // src/cli/lib/login.ts
210
226
  import { confirm, input, password } from "@inquirer/prompts";
211
227
  import { colorize } from "consola/utils";
212
228
  import open from "open";
213
229
  import yoctoSpinner from "yocto-spinner";
214
- import { z as z2 } from "zod";
230
+ import { z as z3 } from "zod";
215
231
 
216
232
  // src/cli/lib/auth.ts
233
+ import { z as z2 } from "zod";
234
+
235
+ // src/cli/lib/http-errors.ts
217
236
  import { z } from "zod";
237
+ var apiErrorBodySchema = z.object({
238
+ title: z.string().optional(),
239
+ detail: z.string().optional(),
240
+ errors: z.record(z.string(), z.string()).optional()
241
+ });
242
+ var FALLBACK_CLIENT_ERROR = "The request was rejected. Check your input \u2014 your access token may be missing a required scope, or the resource may not exist.";
243
+ var FALLBACK_SERVER_ERROR = "Something went wrong on our end. Please try again. If the issue persists, contact support.";
244
+ function extractApiErrorMessage(body) {
245
+ const parsed = apiErrorBodySchema.safeParse(body);
246
+ if (!parsed.success) return void 0;
247
+ const { title, detail, errors } = parsed.data;
248
+ const fieldErrors = errors && Object.keys(errors).length > 0 ? Object.entries(errors).map(([field, message]) => `${field}: ${message}`).join("; ") : void 0;
249
+ const headline = detail ?? title;
250
+ if (headline && fieldErrors) return `${headline} (${fieldErrors})`;
251
+ return headline ?? fieldErrors;
252
+ }
253
+ async function httpError(response, action) {
254
+ const body = await response.json().catch(() => null);
255
+ const isServerError = response.status >= 500;
256
+ const detail = extractApiErrorMessage(body) ?? (isServerError ? FALLBACK_SERVER_ERROR : FALLBACK_CLIENT_ERROR);
257
+ const message = `${action}: ${detail}`;
258
+ return isServerError ? new Error(message) : new UserActionableError(message);
259
+ }
260
+
261
+ // src/cli/lib/auth.ts
218
262
  var DEVICE_OAUTH_CLIENT_ID = "b8063bu6hhml4e0lqh22yut63atsbyv";
219
263
  var DEVICE_OAUTH_SCOPES = [
220
264
  "store_v2_information",
@@ -224,18 +268,18 @@ var DEVICE_OAUTH_SCOPES = [
224
268
  "store_channel_settings"
225
269
  ].join(" ");
226
270
  var DEFAULT_LOGIN_URL = "https://login.bigcommerce.com";
227
- var DeviceCodeResponseSchema = z.object({
228
- device_code: z.string(),
229
- user_code: z.string(),
230
- verification_uri: z.string(),
231
- expires_in: z.number(),
232
- interval: z.number()
233
- });
234
- var DeviceCodeSuccessSchema = z.object({
235
- access_token: z.string(),
236
- store_hash: z.string(),
237
- context: z.string(),
238
- api_uri: z.string()
271
+ var DeviceCodeResponseSchema = z2.object({
272
+ device_code: z2.string(),
273
+ user_code: z2.string(),
274
+ verification_uri: z2.string(),
275
+ expires_in: z2.number(),
276
+ interval: z2.number()
277
+ });
278
+ var DeviceCodeSuccessSchema = z2.object({
279
+ access_token: z2.string(),
280
+ store_hash: z2.string(),
281
+ context: z2.string(),
282
+ api_uri: z2.string()
239
283
  });
240
284
  async function requestDeviceCode(loginUrl) {
241
285
  const response = await fetch(`${loginUrl}/device/token`, {
@@ -247,7 +291,7 @@ async function requestDeviceCode(loginUrl) {
247
291
  })
248
292
  });
249
293
  if (!response.ok) {
250
- throw new Error(`Failed to request device code: ${response.status} ${response.statusText}`);
294
+ throw await httpError(response, "Failed to request device code");
251
295
  }
252
296
  const res = await response.json();
253
297
  return DeviceCodeResponseSchema.parse(res);
@@ -278,6 +322,33 @@ async function waitForDeviceToken(loginUrl, deviceCode, interval) {
278
322
  return waitForDeviceToken(loginUrl, deviceCode, interval);
279
323
  }
280
324
 
325
+ // src/cli/lib/clipboard.ts
326
+ import { execa } from "execa";
327
+ function clipboardCommand() {
328
+ switch (process.platform) {
329
+ case "darwin":
330
+ return { command: "pbcopy", args: [] };
331
+ case "win32":
332
+ return { command: "clip", args: [] };
333
+ case "linux":
334
+ return process.env.WAYLAND_DISPLAY ? { command: "wl-copy", args: [] } : { command: "xclip", args: ["-selection", "clipboard"] };
335
+ default:
336
+ return null;
337
+ }
338
+ }
339
+ async function copyToClipboard(text) {
340
+ const clipboard = clipboardCommand();
341
+ if (!clipboard) {
342
+ return false;
343
+ }
344
+ try {
345
+ await execa(clipboard.command, clipboard.args, { input: text });
346
+ return true;
347
+ } catch {
348
+ return false;
349
+ }
350
+ }
351
+
281
352
  // src/cli/lib/login.ts
282
353
  var LoginAbortedError = class extends Error {
283
354
  constructor() {
@@ -285,9 +356,9 @@ var LoginAbortedError = class extends Error {
285
356
  this.name = "LoginAbortedError";
286
357
  }
287
358
  };
288
- var StoreProfileSchema = z2.object({
289
- data: z2.object({
290
- store_name: z2.string()
359
+ var StoreProfileSchema = z3.object({
360
+ data: z3.object({
361
+ store_name: z3.string()
291
362
  })
292
363
  });
293
364
  async function fetchStoreProfile(storeHash, accessToken, apiHost) {
@@ -313,6 +384,13 @@ async function deviceCodeLogin(loginUrl) {
313
384
  consola.info(
314
385
  `${colorize("yellow", "Your one-time code:")} ${colorize("bold", deviceCode.user_code)}`
315
386
  );
387
+ if (process.stdin.isTTY) {
388
+ await input({ message: "Press Enter to open your browser and sign in" });
389
+ const copied = await copyToClipboard(deviceCode.user_code);
390
+ if (copied) {
391
+ consola.info("Copied the code to your clipboard \u2014 paste it into the sign-in page.");
392
+ }
393
+ }
316
394
  try {
317
395
  await open(deviceCode.verification_uri);
318
396
  consola.info(`Opened ${deviceCode.verification_uri} in your browser.`);
@@ -340,12 +418,12 @@ Grant these OAuth scopes: ${DEVICE_OAUTH_SCOPES}`
340
418
  const storeHashInput = await input({ message: "Store hash:" });
341
419
  const storeHash = storeHashInput.trim();
342
420
  if (!storeHash) {
343
- throw new Error("Store hash is required.");
421
+ throw new UserActionableError("Store hash is required.");
344
422
  }
345
423
  const accessTokenInput = await password({ message: "Access token:", mask: true });
346
424
  const accessToken = accessTokenInput.trim();
347
425
  if (!accessToken) {
348
- throw new Error("Access token is required.");
426
+ throw new UserActionableError("Access token is required.");
349
427
  }
350
428
  const spinner = yoctoSpinner().start("Validating credentials...");
351
429
  try {
@@ -354,7 +432,7 @@ Grant these OAuth scopes: ${DEVICE_OAUTH_SCOPES}`
354
432
  } catch (error) {
355
433
  spinner.error("Failed to validate credentials.");
356
434
  const message = error instanceof Error ? error.message : String(error);
357
- throw new Error(
435
+ throw new UserActionableError(
358
436
  `Could not validate credentials (${message}). Double-check your store hash and access token, then try again.`
359
437
  );
360
438
  }
@@ -378,19 +456,19 @@ async function login(loginUrl, apiHost) {
378
456
  }
379
457
 
380
458
  // src/cli/lib/project.ts
381
- import { z as z3 } from "zod";
382
- var InfrastructureProjectValidationError = class extends Error {
459
+ import { z as z4 } from "zod";
460
+ var InfrastructureProjectValidationError = class extends UserActionableError {
383
461
  constructor(message) {
384
462
  super(message);
385
463
  this.name = "InfrastructureProjectValidationError";
386
464
  }
387
465
  };
388
- var fetchProjectsSchema = z3.object({
389
- data: z3.array(
390
- z3.object({
391
- uuid: z3.string(),
392
- name: z3.string(),
393
- deployment_hostnames: z3.array(z3.string())
466
+ var fetchProjectsSchema = z4.object({
467
+ data: z4.array(
468
+ z4.object({
469
+ uuid: z4.string(),
470
+ name: z4.string(),
471
+ deployment_hostnames: z4.array(z4.string())
394
472
  })
395
473
  )
396
474
  });
@@ -411,9 +489,7 @@ async function hasProjectsAccess(storeHash, accessToken, apiHost) {
411
489
  assertAuthorized(response);
412
490
  if (response.status === 200) return true;
413
491
  if (response.status === 403) return false;
414
- throw new Error(
415
- `GET /v3/infrastructure/projects failed: ${response.status} ${response.statusText}`
416
- );
492
+ throw await httpError(response, "Failed to check project access");
417
493
  }
418
494
  async function fetchProjects(storeHash, accessToken, apiHost) {
419
495
  const response = await fetch(projectsUrl(storeHash, apiHost), {
@@ -422,29 +498,29 @@ async function fetchProjects(storeHash, accessToken, apiHost) {
422
498
  });
423
499
  assertAuthorized(response);
424
500
  if (response.status === 403) {
425
- throw new Error(
426
- "Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
501
+ throw new UserActionableError(
502
+ "Infrastructure Projects API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
427
503
  );
428
504
  }
429
505
  if (!response.ok) {
430
- throw new Error(`Failed to fetch projects: ${response.statusText}`);
506
+ throw await httpError(response, "Failed to fetch projects");
431
507
  }
432
508
  const res = await response.json();
433
509
  const { data } = fetchProjectsSchema.parse(res);
434
510
  return data;
435
511
  }
436
- var createProjectSchema = z3.object({
437
- data: z3.object({
438
- uuid: z3.string(),
439
- name: z3.string(),
440
- date_created: z3.coerce.date(),
441
- date_modified: z3.coerce.date()
512
+ var createProjectSchema = z4.object({
513
+ data: z4.object({
514
+ uuid: z4.string(),
515
+ name: z4.string(),
516
+ date_created: z4.coerce.date(),
517
+ date_modified: z4.coerce.date()
442
518
  })
443
519
  });
444
- var validationErrorBodySchema = z3.object({
445
- title: z3.string().optional(),
446
- detail: z3.string().optional(),
447
- errors: z3.record(z3.string(), z3.string()).optional()
520
+ var validationErrorBodySchema = z4.object({
521
+ title: z4.string().optional(),
522
+ detail: z4.string().optional(),
523
+ errors: z4.record(z4.string(), z4.string()).optional()
448
524
  });
449
525
  function extractValidationMessage(body) {
450
526
  const parsed = validationErrorBodySchema.safeParse(body);
@@ -472,16 +548,16 @@ async function createProject(name, storeHash, accessToken, apiHost) {
472
548
  const message = extractValidationMessage(body) ?? fallback;
473
549
  throw new InfrastructureProjectValidationError(message);
474
550
  }
475
- if (response.status === 403) {
476
- throw new Error(
477
- "Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
551
+ if (response.status === 403 || response.status === 404) {
552
+ throw new UserActionableError(
553
+ "Infrastructure Projects API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
478
554
  );
479
555
  }
480
556
  if (response.status === 502) {
481
557
  throw new Error("Failed to create project, is the name already in use?");
482
558
  }
483
559
  if (!response.ok) {
484
- throw new Error(`Failed to create project: ${response.statusText}`);
560
+ throw await httpError(response, "Failed to create project");
485
561
  }
486
562
  const res = await response.json();
487
563
  const { data } = createProjectSchema.parse(res);
@@ -494,15 +570,15 @@ async function deleteProject(projectUuid, storeHash, accessToken, apiHost) {
494
570
  });
495
571
  assertAuthorized(response);
496
572
  if (response.status === 403) {
497
- throw new Error(
498
- "Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
573
+ throw new UserActionableError(
574
+ "Infrastructure Projects API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
499
575
  );
500
576
  }
501
577
  if (response.status === 404) {
502
- throw new Error(`Project ${projectUuid} not found.`);
578
+ throw new UserActionableError(`Project ${projectUuid} not found.`);
503
579
  }
504
580
  if (!response.ok) {
505
- throw new Error(`Failed to delete project: ${response.statusText}`);
581
+ throw await httpError(response, "Failed to delete project");
506
582
  }
507
583
  }
508
584
 
@@ -523,6 +599,7 @@ function getProjectConfig() {
523
599
  },
524
600
  storeHash: { type: "string" },
525
601
  accessToken: { type: "string" },
602
+ apiHost: { type: "string" },
526
603
  env: {
527
604
  type: "object",
528
605
  additionalProperties: { type: "string" },
@@ -534,6 +611,7 @@ function getProjectConfig() {
534
611
 
535
612
  // src/cli/lib/shared-options.ts
536
613
  import { Option } from "commander";
614
+ var DEFAULT_API_HOST = "api.bigcommerce.com";
537
615
  var storeHashOption = () => new Option(
538
616
  "--store-hash <hash>",
539
617
  "BigCommerce store hash. Can be found in the URL of your store Control Panel. Read from .bigcommerce/project.json or .env when not provided."
@@ -542,12 +620,19 @@ var accessTokenOption = () => new Option(
542
620
  "--access-token <token>",
543
621
  "BigCommerce access token. Can be found after creating a store-level API account. Read from .bigcommerce/project.json or .env when not provided."
544
622
  ).env("CATALYST_ACCESS_TOKEN");
545
- var apiHostOption = () => new Option("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com").hideHelp();
623
+ var apiHostOption = () => new Option(
624
+ "--api-host <host>",
625
+ "BigCommerce API host. Read from .bigcommerce/project.json or CATALYST_API_HOST when not provided. Defaults to api.bigcommerce.com."
626
+ ).env("CATALYST_API_HOST").hideHelp();
546
627
  var loginUrlOption = () => new Option("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp();
547
628
  var projectUuidOption = () => new Option(
548
629
  "--project-uuid <uuid>",
549
630
  "BigCommerce infrastructure project UUID. Read from .bigcommerce/project.json or .env when not provided."
550
631
  ).env("CATALYST_PROJECT_UUID");
632
+ var envPathOption = () => new Option(
633
+ "--env-path <path>",
634
+ "Path to an environment file to load for the build (relative to the current working directory). When omitted, .env.local and .env are auto-loaded from the current directory. Other commands never load env files."
635
+ );
551
636
  var resolveProjectUuid = (options) => {
552
637
  const config2 = getProjectConfig();
553
638
  const projectUuid = options.projectUuid ?? config2.get("projectUuid");
@@ -558,11 +643,12 @@ var resolveProjectUuid = (options) => {
558
643
  }
559
644
  return projectUuid;
560
645
  };
646
+ var resolveApiHost = (options, config2) => options.apiHost ?? config2.get("apiHost") ?? DEFAULT_API_HOST;
561
647
 
562
648
  // src/cli/commands/auth.ts
563
- var StoreProfileSchema2 = z4.object({
564
- data: z4.object({
565
- store_name: z4.string()
649
+ var StoreProfileSchema2 = z5.object({
650
+ data: z5.object({
651
+ store_name: z5.string()
566
652
  })
567
653
  });
568
654
  async function fetchStoreProfile2(storeHash, accessToken, apiHost) {
@@ -591,21 +677,10 @@ Example:
591
677
  $ catalyst auth whoami
592
678
 
593
679
  Logged in to My Store (abc123), connected to project my-project (43eba682-0c48-11f1-9bd5-827a48b0ce1e)`
594
- ).addOption(
595
- new Option2(
596
- "--store-hash <hash>",
597
- "BigCommerce store hash. Can be found in the URL of your store Control Panel."
598
- ).env("CATALYST_STORE_HASH")
599
- ).addOption(
600
- new Option2(
601
- "--access-token <token>",
602
- "BigCommerce access token. Can be found after creating a store-level API account."
603
- ).env("CATALYST_ACCESS_TOKEN")
604
- ).addOption(
605
- new Option2("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com").hideHelp()
606
- ).action(async (options) => {
680
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).action(async (options) => {
607
681
  try {
608
682
  const config2 = getProjectConfig();
683
+ const apiHost = resolveApiHost(options, config2);
609
684
  const storeHash = options.storeHash ?? config2.get("storeHash");
610
685
  const accessToken = options.accessToken ?? config2.get("accessToken");
611
686
  if (!storeHash || !accessToken) {
@@ -616,10 +691,10 @@ Example:
616
691
  process.exit(1);
617
692
  return;
618
693
  }
619
- const store = await fetchStoreProfile2(storeHash, accessToken, options.apiHost);
694
+ const store = await fetchStoreProfile2(storeHash, accessToken, apiHost);
620
695
  const projectUuid = config2.get("projectUuid");
621
696
  if (projectUuid) {
622
- const projects = await fetchProjects(storeHash, accessToken, options.apiHost);
697
+ const projects = await fetchProjects(storeHash, accessToken, apiHost);
623
698
  const linkedProject = projects.find((p) => p.uuid === projectUuid);
624
699
  if (linkedProject) {
625
700
  consola.info(
@@ -659,21 +734,10 @@ Examples:
659
734
 
660
735
  # Login with existing credentials (skips interactive flow)
661
736
  $ catalyst auth login --store-hash <STORE_HASH> --access-token <ACCESS_TOKEN>`
662
- ).addOption(
663
- new Option2(
664
- "--store-hash <hash>",
665
- "BigCommerce store hash. Can be found in the URL of your store Control Panel."
666
- ).env("CATALYST_STORE_HASH")
667
- ).addOption(
668
- new Option2(
669
- "--access-token <token>",
670
- "BigCommerce access token. Can be found after creating a store-level API account."
671
- ).env("CATALYST_ACCESS_TOKEN")
672
- ).addOption(
673
- new Option2("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com").hideHelp()
674
- ).addOption(loginUrlOption()).action(async (options) => {
737
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).action(async (options) => {
675
738
  try {
676
739
  const config2 = getProjectConfig();
740
+ const apiHost = resolveApiHost(options, config2);
677
741
  const storeHash = options.storeHash ?? config2.get("storeHash");
678
742
  const accessToken = options.accessToken ?? config2.get("accessToken");
679
743
  if (storeHash && accessToken) {
@@ -682,7 +746,7 @@ Examples:
682
746
  process.exit(0);
683
747
  return;
684
748
  }
685
- const credentials = await login(options.loginUrl, options.apiHost);
749
+ const credentials = await login(options.loginUrl, apiHost);
686
750
  config2.set("storeHash", credentials.storeHash);
687
751
  config2.set("accessToken", credentials.accessToken);
688
752
  consola.success(`Logged in to store ${credentials.storeHash}.`);
@@ -728,10 +792,43 @@ Example:
728
792
  var auth = new Command("auth").configureHelp({ showGlobalOptions: true }).description("Manage authentication for the BigCommerce CLI.").addCommand(whoami).addCommand(login2).addCommand(logout);
729
793
 
730
794
  // src/cli/commands/build.ts
731
- import { Command as Command2, Option as Option3 } from "commander";
732
- import { execa } from "execa";
795
+ import { Command as Command2, InvalidArgumentError, Option as Option2 } from "commander";
796
+ import { execa as execa2 } from "execa";
733
797
  import { copyFile, cp, rm, writeFile } from "fs/promises";
734
798
  import { join as join4 } from "path";
799
+ import { valid as validSemver } from "semver";
800
+
801
+ // src/cli/lib/build-env.ts
802
+ import { colorize as colorize2 } from "consola/utils";
803
+ import { config } from "dotenv";
804
+ import { existsSync } from "fs";
805
+ import { resolve } from "path";
806
+ var AUTO_ENV_FILES = [".env.local", ".env"];
807
+ function loadBuildEnv({ envPath, cwd = process.cwd() }) {
808
+ if (envPath !== void 0) {
809
+ const envFilePath = resolve(cwd, envPath);
810
+ const result = config({ path: envFilePath, override: true });
811
+ if (result.error) {
812
+ const errCode = "code" in result.error && typeof result.error.code === "string" ? result.error.code : void 0;
813
+ const message = errCode === "ENOENT" ? `Env file not found: ${envFilePath}` : `Failed to load --env-path ${envPath}: ${result.error.message}`;
814
+ throw new UserActionableError(message);
815
+ }
816
+ consola.log(colorize2("cyanBright", `Loaded environment variables from ${envFilePath}
817
+ `));
818
+ return;
819
+ }
820
+ const loaded = AUTO_ENV_FILES.filter((file) => {
821
+ const envFilePath = resolve(cwd, file);
822
+ if (!existsSync(envFilePath)) {
823
+ return false;
824
+ }
825
+ return !config({ path: envFilePath, override: false }).error;
826
+ });
827
+ if (loaded.length > 0) {
828
+ consola.log(colorize2("cyanBright", `Loaded environment variables from ${loaded.join(", ")}
829
+ `));
830
+ }
831
+ }
735
832
 
736
833
  // src/cli/lib/get-module-cli-path.ts
737
834
  import { dirname, join as join2 } from "path";
@@ -743,14 +840,14 @@ function getModuleCliPath() {
743
840
  }
744
841
 
745
842
  // src/cli/lib/project-state.ts
746
- import { existsSync, readFileSync } from "fs";
843
+ import { existsSync as existsSync2, readFileSync } from "fs";
747
844
  import { join as join3 } from "path";
748
- import { z as z5 } from "zod";
749
- var projectJsonSchema = z5.looseObject({
750
- projectUuid: z5.string().optional()
845
+ import { z as z6 } from "zod";
846
+ var projectJsonSchema = z6.looseObject({
847
+ projectUuid: z6.string().optional()
751
848
  });
752
- var packageJsonSchema = z5.looseObject({
753
- dependencies: z5.record(z5.string(), z5.string()).optional()
849
+ var packageJsonSchema = z6.looseObject({
850
+ dependencies: z6.record(z6.string(), z6.string()).optional()
754
851
  });
755
852
  var safeReadJson = (path) => {
756
853
  try {
@@ -764,8 +861,8 @@ function getProjectState(cwd = process.cwd()) {
764
861
  safeReadJson(join3(cwd, ".bigcommerce", "project.json"))
765
862
  );
766
863
  const projectUuid = projectJson.success ? projectJson.data.projectUuid : void 0;
767
- const hasMiddleware = existsSync(join3(cwd, "middleware.ts"));
768
- const hasProxy = existsSync(join3(cwd, "proxy.ts"));
864
+ const hasMiddleware = existsSync2(join3(cwd, "middleware.ts"));
865
+ const hasProxy = existsSync2(join3(cwd, "proxy.ts"));
769
866
  const pkgJson = packageJsonSchema.safeParse(safeReadJson(join3(cwd, "package.json")));
770
867
  const hasOpenNextDep = pkgJson.success ? Boolean(pkgJson.data.dependencies?.["@opennextjs/cloudflare"]) : false;
771
868
  const isLinked = Boolean(projectUuid);
@@ -782,6 +879,34 @@ function getProjectState(cwd = process.cwd()) {
782
879
  };
783
880
  }
784
881
 
882
+ // src/cli/lib/required-build-env.ts
883
+ var REQUIRED_BUILD_ENV_VARS = [
884
+ "BIGCOMMERCE_STORE_HASH",
885
+ "BIGCOMMERCE_STOREFRONT_TOKEN",
886
+ "BIGCOMMERCE_CHANNEL_ID",
887
+ "AUTH_SECRET"
888
+ ];
889
+ var findMissingBuildEnv = (env2 = process.env) => {
890
+ return REQUIRED_BUILD_ENV_VARS.filter((name) => {
891
+ const value = env2[name];
892
+ return value === void 0 || value.trim() === "";
893
+ });
894
+ };
895
+ var assertRequiredBuildEnv = (env2 = process.env) => {
896
+ const missing = findMissingBuildEnv(env2);
897
+ if (missing.length === 0) {
898
+ return;
899
+ }
900
+ const missingList = missing.map((name) => ` - ${name}`).join("\n");
901
+ throw new UserActionableError(
902
+ `Missing required environment variable${missing.length === 1 ? "" : "s"} for the build:
903
+ ${missingList}
904
+
905
+ These are read by the build itself, so they must be set before it runs. The build automatically loads \`.env.local\` and \`.env\` from the current directory (${process.cwd()}) \u2014 add the variables above to one of those files (\`.env.local\` takes precedence).
906
+ To load an env file from a different location, pass \`--env-path <path>\` (for example \`--env-path ../.env.local\` when running from a subdirectory).`
907
+ );
908
+ };
909
+
785
910
  // src/cli/lib/wrangler-config.ts
786
911
  function getCompatibilityDate(now = /* @__PURE__ */ new Date()) {
787
912
  const date = new Date(now);
@@ -847,7 +972,17 @@ function getWranglerConfig(projectUuid) {
847
972
 
848
973
  // src/cli/commands/build.ts
849
974
  var WRANGLER_VERSION = "4.90.0";
850
- async function buildCatalystProject(projectUuid) {
975
+ var DIST_TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
976
+ var parseWranglerVersion = (value) => {
977
+ if (!validSemver(value) && !DIST_TAG_PATTERN.test(value)) {
978
+ throw new InvalidArgumentError(
979
+ `"${value}" is not a valid Wrangler version or dist-tag (e.g. 4.90.0 or latest).`
980
+ );
981
+ }
982
+ return value;
983
+ };
984
+ async function buildCatalystProject(projectUuid, wranglerVersion = WRANGLER_VERSION) {
985
+ assertRequiredBuildEnv();
851
986
  const coreDir = process.cwd();
852
987
  const openNextOutDir = join4(coreDir, ".open-next");
853
988
  const bigcommerceDistDir = join4(coreDir, ".bigcommerce", "dist");
@@ -864,7 +999,7 @@ async function buildCatalystProject(projectUuid) {
864
999
  );
865
1000
  consola.success("Templates copied");
866
1001
  consola.start("Building project...");
867
- await execa(
1002
+ await execa2(
868
1003
  "pnpm",
869
1004
  [
870
1005
  "exec",
@@ -879,11 +1014,11 @@ async function buildCatalystProject(projectUuid) {
879
1014
  cwd: coreDir
880
1015
  }
881
1016
  );
882
- await execa(
1017
+ await execa2(
883
1018
  "pnpm",
884
1019
  [
885
1020
  "dlx",
886
- `wrangler@${WRANGLER_VERSION}`,
1021
+ `wrangler@${wranglerVersion}`,
887
1022
  "deploy",
888
1023
  "--config",
889
1024
  join4(coreDir, ".bigcommerce", "wrangler.jsonc"),
@@ -912,18 +1047,28 @@ Examples:
912
1047
  $ catalyst build
913
1048
 
914
1049
  # Include project UUID
915
- $ catalyst build --project-uuid <UUID>`
1050
+ $ catalyst build --project-uuid <UUID>
1051
+
1052
+ # Build with a specific Wrangler version
1053
+ $ catalyst build --wrangler-version 4.24.3`
916
1054
  ).addOption(
917
- new Option3(
1055
+ new Option2(
918
1056
  "--project-uuid <uuid>",
919
1057
  "Project UUID to be included in the deployment configuration."
920
1058
  ).env("CATALYST_PROJECT_UUID")
921
- ).action(async (options) => {
1059
+ ).addOption(
1060
+ new Option2(
1061
+ "--wrangler-version <version>",
1062
+ `Wrangler version or dist-tag to build with. Defaults to ${WRANGLER_VERSION}.`
1063
+ ).argParser(parseWranglerVersion)
1064
+ ).addOption(envPathOption()).action(async (options) => {
1065
+ loadBuildEnv({ envPath: options.envPath });
922
1066
  const state = getProjectState();
923
1067
  if (!state.isTransformed) {
924
1068
  consola.info("Project is not set up for Commerce Hosting \u2014 running `next build`.");
925
1069
  consola.info("To deploy to Commerce Hosting, run `catalyst deploy`.");
926
- await execa("pnpm", ["exec", "next", "build"], {
1070
+ assertRequiredBuildEnv();
1071
+ await execa2("pnpm", ["exec", "next", "build"], {
927
1072
  stdio: "inherit",
928
1073
  cwd: process.cwd()
929
1074
  });
@@ -936,21 +1081,19 @@ Examples:
936
1081
  "Project UUID is required. Please run `catalyst project create` or `catalyst project link` or this command again with --project-uuid <uuid>."
937
1082
  );
938
1083
  }
939
- await buildCatalystProject(projectUuid);
1084
+ await buildCatalystProject(projectUuid, options.wranglerVersion);
940
1085
  });
941
1086
 
942
1087
  // src/cli/commands/channel.ts
943
- import { select as select3 } from "@inquirer/prompts";
944
- import { Command as Command3, InvalidArgumentError, Option as Option4 } from "commander";
945
- import { colorize as colorize3 } from "consola/utils";
946
- import { outputFileSync } from "fs-extra/esm";
947
- import { join as join6 } from "path";
1088
+ import { select as select4 } from "@inquirer/prompts";
1089
+ import { Command as Command3, InvalidArgumentError as InvalidArgumentError2, Option as Option3 } from "commander";
1090
+ import { colorize as colorize5 } from "consola/utils";
948
1091
 
949
1092
  // src/cli/lib/channel-site-flow.ts
950
1093
  import { select as select2 } from "@inquirer/prompts";
951
1094
 
952
1095
  // src/cli/lib/channels.ts
953
- import { z as z6 } from "zod";
1096
+ import { z as z7 } from "zod";
954
1097
  var cliApiUrl = (origin, storeHash, path) => `${origin}/stores/${storeHash}/cli-api/v3${path}`;
955
1098
  var channelsUrl = (storeHash, apiHost, query2 = {}) => {
956
1099
  const params = new URLSearchParams(query2).toString();
@@ -961,14 +1104,14 @@ var authHeaders2 = (accessToken) => ({
961
1104
  "X-Correlation-Id": getTelemetry().correlationId,
962
1105
  Accept: "application/json"
963
1106
  });
964
- var envVarsSchema = z6.record(z6.string(), z6.coerce.string());
965
- var channelSchema = z6.object({
966
- id: z6.number(),
967
- name: z6.string(),
968
- platform: z6.string()
1107
+ var envVarsSchema = z7.record(z7.string(), z7.coerce.string());
1108
+ var channelSchema = z7.object({
1109
+ id: z7.number(),
1110
+ name: z7.string(),
1111
+ platform: z7.string()
969
1112
  });
970
- var channelsResponseSchema = z6.object({
971
- data: z6.array(channelSchema)
1113
+ var channelsResponseSchema = z7.object({
1114
+ data: z7.array(channelSchema)
972
1115
  });
973
1116
  var CHANNEL_PLATFORM_ORDER = ["catalyst", "next", "bigcommerce"];
974
1117
  function sortChannelsByPlatform(channels) {
@@ -984,30 +1127,30 @@ function sortChannelsByPlatform(channels) {
984
1127
  function channelPlatformLabel(platform) {
985
1128
  return platform === "bigcommerce" ? "Stencil" : platform.charAt(0).toUpperCase() + platform.slice(1);
986
1129
  }
987
- var initResponseSchema = z6.object({
988
- data: z6.object({
989
- storefront_api_token: z6.string(),
1130
+ var initResponseSchema = z7.object({
1131
+ data: z7.object({
1132
+ storefront_api_token: z7.string(),
990
1133
  envVars: envVarsSchema
991
1134
  })
992
1135
  });
993
- var createChannelResponseSchema = z6.object({
994
- data: z6.object({
995
- id: z6.number(),
996
- storefront_api_token: z6.string(),
1136
+ var createChannelResponseSchema = z7.object({
1137
+ data: z7.object({
1138
+ id: z7.number(),
1139
+ storefront_api_token: z7.string(),
997
1140
  envVars: envVarsSchema
998
1141
  })
999
1142
  });
1000
- var eligibilityResponseSchema = z6.object({
1001
- data: z6.object({
1002
- eligible: z6.boolean(),
1003
- message: z6.string()
1143
+ var eligibilityResponseSchema = z7.object({
1144
+ data: z7.object({
1145
+ eligible: z7.boolean(),
1146
+ message: z7.string()
1004
1147
  })
1005
1148
  });
1006
- var channelSiteSchema = z6.object({
1007
- data: z6.object({
1008
- id: z6.number(),
1009
- url: z6.string(),
1010
- channel_id: z6.number()
1149
+ var channelSiteSchema = z7.object({
1150
+ data: z7.object({
1151
+ id: z7.number(),
1152
+ url: z7.string(),
1153
+ channel_id: z7.number()
1011
1154
  })
1012
1155
  });
1013
1156
  async function getChannelInit(channelId, storeHash, accessToken, origin) {
@@ -1016,9 +1159,7 @@ async function getChannelInit(channelId, storeHash, accessToken, origin) {
1016
1159
  headers: authHeaders2(accessToken)
1017
1160
  });
1018
1161
  if (!response.ok) {
1019
- throw new Error(
1020
- `GET /channels/${channelId}/init failed: ${response.status} ${response.statusText}`
1021
- );
1162
+ throw await httpError(response, "Failed to initialize channel");
1022
1163
  }
1023
1164
  const { data } = initResponseSchema.parse(await response.json());
1024
1165
  return { storefrontToken: data.storefront_api_token, envVars: data.envVars };
@@ -1029,9 +1170,7 @@ async function checkChannelEligibility(storeHash, accessToken, origin) {
1029
1170
  headers: authHeaders2(accessToken)
1030
1171
  });
1031
1172
  if (!response.ok) {
1032
- throw new Error(
1033
- `GET /channels/catalyst/eligibility failed: ${response.status} ${response.statusText}`
1034
- );
1173
+ throw await httpError(response, "Failed to check Catalyst eligibility");
1035
1174
  }
1036
1175
  return eligibilityResponseSchema.parse(await response.json()).data;
1037
1176
  }
@@ -1049,7 +1188,7 @@ async function createChannel(name, storefrontLocale, additionalLocales, installS
1049
1188
  })
1050
1189
  });
1051
1190
  if (!response.ok) {
1052
- throw new Error(`POST /channels/catalyst failed: ${response.status} ${response.statusText}`);
1191
+ throw await httpError(response, "Failed to create channel");
1053
1192
  }
1054
1193
  const { data } = createChannelResponseSchema.parse(await response.json());
1055
1194
  return {
@@ -1072,7 +1211,7 @@ async function fetchAvailableChannels(storeHash, accessToken, apiHost) {
1072
1211
  );
1073
1212
  assertAuthorized(response);
1074
1213
  if (!response.ok) {
1075
- throw new Error(`GET /v3/channels failed: ${response.status} ${response.statusText}`);
1214
+ throw await httpError(response, "Failed to fetch channels");
1076
1215
  }
1077
1216
  return channelsResponseSchema.parse(await response.json()).data;
1078
1217
  }
@@ -1091,12 +1230,12 @@ async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken,
1091
1230
  }
1092
1231
  );
1093
1232
  if (response.status === 401 || response.status === 403) {
1094
- throw new Error(
1233
+ throw new UserActionableError(
1095
1234
  `Failed to update channel site (${response.status}). Re-run \`catalyst auth login\` to refresh your access token with the store_channel_settings scope.`
1096
1235
  );
1097
1236
  }
1098
1237
  if (!response.ok) {
1099
- throw new Error(`Failed to update channel site: ${response.status} ${response.statusText}`);
1238
+ throw await httpError(response, "Failed to update channel site");
1100
1239
  }
1101
1240
  const res = await response.json();
1102
1241
  const { data } = channelSiteSchema.parse(res);
@@ -1105,18 +1244,10 @@ async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken,
1105
1244
 
1106
1245
  // src/cli/lib/commerce-hosting.ts
1107
1246
  import { confirm as confirm2, input as input2, select, Separator } from "@inquirer/prompts";
1108
- import { colorize as colorize2 } from "consola/utils";
1109
- import {
1110
- existsSync as existsSync2,
1111
- lstatSync,
1112
- mkdirSync,
1113
- readFileSync as readFileSync2,
1114
- symlinkSync,
1115
- unlinkSync,
1116
- writeFileSync
1117
- } from "fs";
1247
+ import { colorize as colorize3 } from "consola/utils";
1248
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1118
1249
  import { dirname as dirname2, join as join5 } from "path";
1119
- import { z as z7 } from "zod";
1250
+ import { z as z8 } from "zod";
1120
1251
 
1121
1252
  // src/cli/lib/sort-package-json.ts
1122
1253
  var FIELD_ORDER = [
@@ -1146,65 +1277,51 @@ function sortPackageJsonFields(pkg) {
1146
1277
 
1147
1278
  // src/cli/lib/commerce-hosting.ts
1148
1279
  var OPENNEXT_CLOUDFLARE_VERSION = "1.17.3";
1149
- var corePackageJsonSchema = z7.looseObject({
1150
- dependencies: z7.record(z7.string(), z7.string()).optional()
1280
+ var corePackageJsonSchema = z8.looseObject({
1281
+ dependencies: z8.record(z8.string(), z8.string()).optional()
1151
1282
  });
1152
1283
  var writeJson = (path, value) => {
1153
1284
  mkdirSync(dirname2(path), { recursive: true });
1154
1285
  writeFileSync(path, `${JSON.stringify(value, null, 2)}
1155
1286
  `);
1156
1287
  };
1157
- var symlinkRootEnvToCore = (projectDir) => {
1158
- const coreEnvPath = join5(projectDir, "core", ".env.local");
1159
- if (lstatSync(coreEnvPath, { throwIfNoEntry: false })) return;
1160
- try {
1161
- symlinkSync("../.env.local", coreEnvPath);
1162
- } catch (error) {
1163
- const message = error instanceof Error ? error.message : "unknown error";
1164
- consola.warn(
1165
- `Could not create symlink at core/.env.local: ${message}
1166
- On Windows, enable Developer Mode or run as administrator to allow symlinks.
1167
- You will need to keep .env.local and core/.env.local in sync manually.`
1168
- );
1169
- }
1170
- };
1171
1288
  var convertProxyToMiddleware = (projectDir) => {
1172
- const proxyPath = join5(projectDir, "core", "proxy.ts");
1173
- const middlewarePath = join5(projectDir, "core", "middleware.ts");
1174
- if (!existsSync2(proxyPath)) return;
1289
+ const proxyPath = join5(projectDir, "proxy.ts");
1290
+ const middlewarePath = join5(projectDir, "middleware.ts");
1291
+ if (!existsSync3(proxyPath)) return;
1175
1292
  const contents = readFileSync2(proxyPath, "utf-8").replace("export const proxy", "export const middleware").replace("export const config = {", "export const config = {\n runtime: 'experimental-edge',");
1176
1293
  writeFileSync(middlewarePath, contents);
1177
1294
  unlinkSync(proxyPath);
1178
1295
  };
1179
1296
  var cleanupCloudflareIncompatibilities = async (projectDir) => {
1180
- const instrumentationPath = join5(projectDir, "core", "instrumentation.ts");
1181
- if (!existsSync2(instrumentationPath)) return;
1297
+ const instrumentationPath = join5(projectDir, "instrumentation.ts");
1298
+ if (!existsSync3(instrumentationPath)) return;
1182
1299
  if (!process.stdin.isTTY) {
1183
1300
  consola.warn(
1184
- "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."
1301
+ "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."
1185
1302
  );
1186
1303
  return;
1187
1304
  }
1188
1305
  const shouldRemove = await confirm2({
1189
- message: '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?',
1306
+ 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?',
1190
1307
  default: true
1191
1308
  });
1192
1309
  if (!shouldRemove) {
1193
1310
  consola.info(
1194
- '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.'
1311
+ 'Leaving instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
1195
1312
  );
1196
1313
  return;
1197
1314
  }
1198
1315
  unlinkSync(instrumentationPath);
1199
- consola.info("Removed core/instrumentation.ts (incompatible with Cloudflare Workers).");
1200
- const corePackageJsonPath = join5(projectDir, "core", "package.json");
1201
- if (existsSync2(corePackageJsonPath)) {
1316
+ consola.info("Removed instrumentation.ts (incompatible with Cloudflare Workers).");
1317
+ const corePackageJsonPath = join5(projectDir, "package.json");
1318
+ if (existsSync3(corePackageJsonPath)) {
1202
1319
  const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
1203
1320
  if (pkg.dependencies && "@vercel/otel" in pkg.dependencies) {
1204
1321
  const { "@vercel/otel": _removedVercelOtel, ...preservedDeps } = pkg.dependencies;
1205
1322
  pkg.dependencies = preservedDeps;
1206
1323
  writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
1207
- consola.info("Dropped @vercel/otel from core/package.json.");
1324
+ consola.info("Dropped @vercel/otel from package.json.");
1208
1325
  }
1209
1326
  }
1210
1327
  };
@@ -1215,7 +1332,7 @@ var setupCommerceHosting = async ({
1215
1332
  accessToken
1216
1333
  }) => {
1217
1334
  await cleanupCloudflareIncompatibilities(projectDir);
1218
- const corePackageJsonPath = join5(projectDir, "core", "package.json");
1335
+ const corePackageJsonPath = join5(projectDir, "package.json");
1219
1336
  const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
1220
1337
  pkg.dependencies = {
1221
1338
  ...pkg.dependencies,
@@ -1228,8 +1345,7 @@ var setupCommerceHosting = async ({
1228
1345
  };
1229
1346
  if (storeHash) projectJson.storeHash = storeHash;
1230
1347
  if (accessToken) projectJson.accessToken = accessToken;
1231
- writeJson(join5(projectDir, "core", ".bigcommerce", "project.json"), projectJson);
1232
- symlinkRootEnvToCore(projectDir);
1348
+ writeJson(join5(projectDir, ".bigcommerce", "project.json"), projectJson);
1233
1349
  convertProxyToMiddleware(projectDir);
1234
1350
  };
1235
1351
  var NoLinkedProjectError = class extends Error {
@@ -1260,7 +1376,7 @@ async function selectOrCreateInfrastructureProject(api, linkedProjectUuid) {
1260
1376
  }
1261
1377
  const choices = [
1262
1378
  ...existingProjects.map((p) => ({
1263
- name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize2("green", "[linked]")}` : p.name,
1379
+ name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize3("green", "[linked]")}` : p.name,
1264
1380
  value: p.uuid,
1265
1381
  description: p.uuid
1266
1382
  })),
@@ -1357,7 +1473,7 @@ async function promptAndCreateCommerceHostingProject(api, takenNames, defaultNam
1357
1473
  },
1358
1474
  theme: {
1359
1475
  style: {
1360
- help: () => colorize2(
1476
+ help: () => colorize3(
1361
1477
  "dim",
1362
1478
  "(The project that hosts your storefront on Commerce \u2014 often matches your folder name.)"
1363
1479
  )
@@ -1503,17 +1619,149 @@ async function runChannelSiteUrlFlow(options) {
1503
1619
  consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
1504
1620
  }
1505
1621
 
1622
+ // src/cli/lib/create-channel-flow.ts
1623
+ import { checkbox, input as input3, select as select3 } from "@inquirer/prompts";
1624
+ import { colorize as colorize4 } from "consola/utils";
1625
+
1626
+ // src/cli/lib/localization.ts
1627
+ import { z as z9 } from "zod";
1628
+ var allowedLocales = [
1629
+ "en",
1630
+ "da",
1631
+ "es-AR",
1632
+ "es-CL",
1633
+ "es-CO",
1634
+ "es-MX",
1635
+ "es-PE",
1636
+ "es-419",
1637
+ "es",
1638
+ "it",
1639
+ "nl",
1640
+ "pl",
1641
+ "pt",
1642
+ "de",
1643
+ "fr",
1644
+ "ja",
1645
+ "no",
1646
+ "pt-BR",
1647
+ "sv"
1648
+ ];
1649
+ var AvailableLocalesSuccessSchema = z9.object({
1650
+ data: z9.array(
1651
+ z9.object({
1652
+ id: z9.string(),
1653
+ name: z9.string(),
1654
+ fallback: z9.string().nullable(),
1655
+ is_supported: z9.boolean()
1656
+ })
1657
+ )
1658
+ });
1659
+ var getAvailableLocales = async (storeHash, accessToken, apiHost) => {
1660
+ const response = await fetch(
1661
+ `https://${apiHost}/stores/${storeHash}/v3/settings/store/available-locales`,
1662
+ {
1663
+ method: "GET",
1664
+ headers: {
1665
+ "X-Auth-Token": accessToken,
1666
+ "X-Correlation-Id": getTelemetry().correlationId,
1667
+ Accept: "application/json"
1668
+ }
1669
+ }
1670
+ );
1671
+ assertAuthorized(response);
1672
+ if (!response.ok) {
1673
+ throw await httpError(response, "Failed to fetch available locales");
1674
+ }
1675
+ return AvailableLocalesSuccessSchema.parse(await response.json()).data.filter(({ id }) => allowedLocales.includes(id)).map(({ name, id }) => ({ name: `${name} (${id})`, value: id }));
1676
+ };
1677
+
1678
+ // src/cli/lib/create-channel-flow.ts
1679
+ var ALLOWED_CHANNEL_NAME_CHARS = "letters, numbers, spaces, hyphens (-), and underscores (_)";
1680
+ var CHANNEL_NAME_PATTERN = /^[\p{L}\p{N} _-]+$/u;
1681
+ function getChannelNameError(name) {
1682
+ if (name.trim().length === 0) {
1683
+ return "Channel name cannot be empty.";
1684
+ }
1685
+ if (!CHANNEL_NAME_PATTERN.test(name)) {
1686
+ return `"${name}" is not a valid channel name. Channel names may contain only ${ALLOWED_CHANNEL_NAME_CHARS}.`;
1687
+ }
1688
+ return void 0;
1689
+ }
1690
+ async function runCreateChannelFlow(options) {
1691
+ const { storeHash, accessToken, apiHost, cliApiOrigin } = options;
1692
+ if (options.name !== void 0) {
1693
+ const nameError = getChannelNameError(options.name);
1694
+ if (nameError) {
1695
+ throw new UserActionableError(nameError);
1696
+ }
1697
+ }
1698
+ const name = options.name ?? await input3({
1699
+ message: "What would you like to name your new channel?",
1700
+ validate: (value) => getChannelNameError(value) ?? true
1701
+ });
1702
+ let availableLocales;
1703
+ const loadLocales = async () => {
1704
+ availableLocales ?? (availableLocales = await getAvailableLocales(storeHash, accessToken, apiHost));
1705
+ return availableLocales;
1706
+ };
1707
+ const storefrontLocale = options.locale ?? await select3({
1708
+ message: "Which default language would you like to set for your channel?",
1709
+ default: "en",
1710
+ choices: await loadLocales(),
1711
+ theme: {
1712
+ style: {
1713
+ help: () => colorize4("dim", "(Select locale from the list or start typing the name)")
1714
+ }
1715
+ }
1716
+ });
1717
+ let additionalLocales = options.additionalLocales;
1718
+ if (additionalLocales === void 0) {
1719
+ const shouldAddAdditionalLocales = await select3({
1720
+ message: "Would you like to add additional languages?",
1721
+ choices: [
1722
+ { name: "Yes", value: true },
1723
+ { name: "No", value: false }
1724
+ ]
1725
+ });
1726
+ additionalLocales = [];
1727
+ if (shouldAddAdditionalLocales) {
1728
+ const localeChoices = (await loadLocales()).filter(({ value }) => value !== storefrontLocale).map(({ name: localeName, value }) => ({ name: localeName, value, description: value }));
1729
+ additionalLocales = await checkbox({
1730
+ message: "Which additional languages would you like to add to your channel?",
1731
+ choices: localeChoices,
1732
+ validate: (items) => items.length <= 4 || "You can only select up to 4 additional languages."
1733
+ });
1734
+ }
1735
+ }
1736
+ const shouldInstallSampleData = options.sampleData ?? await select3({
1737
+ message: "Would you like to install sample data?",
1738
+ choices: [
1739
+ { name: "Yes", value: true },
1740
+ { name: "No", value: false }
1741
+ ]
1742
+ });
1743
+ return createChannel(
1744
+ name,
1745
+ storefrontLocale,
1746
+ additionalLocales,
1747
+ shouldInstallSampleData,
1748
+ storeHash,
1749
+ accessToken,
1750
+ cliApiOrigin
1751
+ );
1752
+ }
1753
+
1506
1754
  // src/cli/lib/env-config.ts
1507
1755
  var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
1508
- var parseEnvAssignment = (input5) => {
1509
- const separatorIndex = input5.indexOf("=");
1756
+ var parseEnvAssignment = (input6) => {
1757
+ const separatorIndex = input6.indexOf("=");
1510
1758
  if (separatorIndex === -1) {
1511
- throw new Error(`Invalid env var format: ${input5}. Expected format: KEY=VALUE`);
1759
+ throw new Error(`Invalid env var format: ${input6}. Expected format: KEY=VALUE`);
1512
1760
  }
1513
- const key = input5.slice(0, separatorIndex).trim();
1514
- const value = input5.slice(separatorIndex + 1).trim();
1761
+ const key = input6.slice(0, separatorIndex).trim();
1762
+ const value = input6.slice(separatorIndex + 1).trim();
1515
1763
  if (!key || !value) {
1516
- throw new Error(`Invalid env var format: ${input5}. Expected format: KEY=VALUE`);
1764
+ throw new Error(`Invalid env var format: ${input6}. Expected format: KEY=VALUE`);
1517
1765
  }
1518
1766
  if (!ENV_KEY_PATTERN.test(key)) {
1519
1767
  throw new Error(
@@ -1550,22 +1798,89 @@ function resolveCredentials(options, config2) {
1550
1798
  return { storeHash, accessToken };
1551
1799
  }
1552
1800
 
1801
+ // src/cli/lib/write-env.ts
1802
+ import { outputFileSync } from "fs-extra/esm";
1803
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1804
+ import { join as join6 } from "path";
1805
+ var ACTIVE_KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
1806
+ var keyFromLine = (line) => {
1807
+ const match = ACTIVE_KEY_LINE.exec(line);
1808
+ return match ? match[1] : null;
1809
+ };
1810
+ var parseEnvValues = (contents) => {
1811
+ const values = /* @__PURE__ */ new Map();
1812
+ contents.split("\n").forEach((line) => {
1813
+ const match = ACTIVE_KEY_LINE.exec(line);
1814
+ if (match) {
1815
+ values.set(match[1], match[2]);
1816
+ }
1817
+ });
1818
+ return values;
1819
+ };
1820
+ var writeEnv = (projectDir, envVars) => {
1821
+ const examplePath = join6(projectDir, ".env.example");
1822
+ const localPath = join6(projectDir, ".env.local");
1823
+ const existingLocal = existsSync4(localPath) ? parseEnvValues(readFileSync3(localPath, "utf-8")) : /* @__PURE__ */ new Map();
1824
+ const resolve2 = (key) => {
1825
+ if (key in envVars && envVars[key] !== "") {
1826
+ return envVars[key];
1827
+ }
1828
+ return existingLocal.get(key);
1829
+ };
1830
+ if (!existsSync4(examplePath)) {
1831
+ const keys = [.../* @__PURE__ */ new Set([...existingLocal.keys(), ...Object.keys(envVars)])];
1832
+ outputFileSync(localPath, `${keys.map((key) => `${key}=${resolve2(key) ?? ""}`).join("\n")}
1833
+ `);
1834
+ return;
1835
+ }
1836
+ const exampleLines = readFileSync3(examplePath, "utf-8").replace(/\n$/, "").split("\n");
1837
+ const exampleKeys = /* @__PURE__ */ new Set();
1838
+ const lines = exampleLines.map((line) => {
1839
+ const key = keyFromLine(line);
1840
+ if (key === null) {
1841
+ return line;
1842
+ }
1843
+ exampleKeys.add(key);
1844
+ const value = resolve2(key);
1845
+ return value === void 0 ? line : `${key}=${value}`;
1846
+ });
1847
+ const extraKeys = [];
1848
+ [...existingLocal.keys()].forEach((key) => {
1849
+ if (!exampleKeys.has(key)) {
1850
+ extraKeys.push(key);
1851
+ }
1852
+ });
1853
+ Object.keys(envVars).forEach((key) => {
1854
+ if (!exampleKeys.has(key) && !extraKeys.includes(key)) {
1855
+ extraKeys.push(key);
1856
+ }
1857
+ });
1858
+ if (extraKeys.length > 0) {
1859
+ lines.push("", "# Additional variables set by the Catalyst CLI (not in .env.example).");
1860
+ extraKeys.forEach((key) => {
1861
+ lines.push(`${key}=${resolve2(key) ?? ""}`);
1862
+ });
1863
+ }
1864
+ outputFileSync(localPath, `${lines.join("\n")}
1865
+ `);
1866
+ };
1867
+
1553
1868
  // src/cli/commands/channel.ts
1554
1869
  var parseChannelId = (value) => {
1555
1870
  const parsed = Number.parseInt(value, 10);
1556
1871
  if (Number.isNaN(parsed)) {
1557
- throw new InvalidArgumentError(`"${value}" is not a valid channel ID (expected a number).`);
1872
+ throw new InvalidArgumentError2(`"${value}" is not a valid channel ID (expected a number).`);
1558
1873
  }
1559
1874
  return parsed;
1560
1875
  };
1561
- async function resolveCredentialsWithLogin(options, config2) {
1876
+ async function resolveCredentialsWithLogin(options, config2, apiHost) {
1562
1877
  const storeHash = options.storeHash ?? config2.get("storeHash");
1563
1878
  const accessToken = options.accessToken ?? config2.get("accessToken");
1564
1879
  if (storeHash && accessToken) {
1565
1880
  return { storeHash, accessToken };
1566
1881
  }
1567
1882
  try {
1568
- const credentials = await login(options.loginUrl, options.apiHost);
1883
+ const credentials = await login(options.loginUrl, apiHost);
1569
1884
  config2.set("storeHash", credentials.storeHash);
1570
1885
  config2.set("accessToken", credentials.accessToken);
1571
1886
  return credentials;
@@ -1588,24 +1903,25 @@ Examples:
1588
1903
  # Skip both prompts
1589
1904
  $ catalyst channel update --channel-id 123 --hostname my-storefront.example.com`
1590
1905
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
1591
- new Option4(
1906
+ new Option3(
1592
1907
  "--channel-id <id>",
1593
1908
  "Skip the channel prompt and target this channel directly."
1594
1909
  ).argParser((value) => Number(value))
1595
1910
  ).addOption(
1596
- new Option4(
1911
+ new Option3(
1597
1912
  "--hostname <hostname>",
1598
1913
  "Skip the hostname prompt and use this hostname directly. Must be one of the project's deployment_hostnames."
1599
1914
  )
1600
1915
  ).action(async (options) => {
1601
1916
  const config2 = getProjectConfig();
1917
+ const apiHost = resolveApiHost(options, config2);
1602
1918
  const { storeHash, accessToken } = resolveCredentials(options, config2);
1603
1919
  await getTelemetry().identify(storeHash);
1604
1920
  try {
1605
1921
  await runChannelSiteUrlFlow({
1606
1922
  storeHash,
1607
1923
  accessToken,
1608
- apiHost: options.apiHost,
1924
+ apiHost,
1609
1925
  projectUuid: options.projectUuid ?? config2.get("projectUuid"),
1610
1926
  channelId: options.channelId,
1611
1927
  hostname: options.hostname
@@ -1637,17 +1953,18 @@ Examples:
1637
1953
  # Append extra environment variables to .env.local
1638
1954
  $ catalyst channel link --channel-id 123 --env MY_FLAG=1`
1639
1955
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).addOption(
1640
- new Option4("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
1956
+ new Option3("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
1641
1957
  parseChannelId
1642
1958
  )
1643
1959
  ).option(
1644
1960
  "--env <vars...>",
1645
1961
  "Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable)."
1646
1962
  ).addOption(
1647
- new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
1963
+ new Option3("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
1648
1964
  ).action(async (options) => {
1649
1965
  const config2 = getProjectConfig();
1650
- const credentials = await resolveCredentialsWithLogin(options, config2);
1966
+ const apiHost = resolveApiHost(options, config2);
1967
+ const credentials = await resolveCredentialsWithLogin(options, config2, apiHost);
1651
1968
  if (!credentials) {
1652
1969
  consola.info(
1653
1970
  "Login aborted. Re-run `catalyst channel link` when you have your credentials ready."
@@ -1660,7 +1977,7 @@ Examples:
1660
1977
  let channelId = options.channelId;
1661
1978
  let channelName;
1662
1979
  if (channelId === void 0) {
1663
- const channels = await fetchAvailableChannels(storeHash, accessToken, options.apiHost);
1980
+ const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
1664
1981
  if (channels.length === 0) {
1665
1982
  consola.info(
1666
1983
  "No storefront channels found on this store. Create one with `catalyst create` and try again."
@@ -1668,7 +1985,7 @@ Examples:
1668
1985
  process.exit(0);
1669
1986
  return;
1670
1987
  }
1671
- channelId = await select3({
1988
+ channelId = await select4({
1672
1989
  message: "Which channel would you like to link?",
1673
1990
  choices: sortChannelsByPlatform(channels).map((c) => ({
1674
1991
  name: c.name,
@@ -1686,164 +2003,226 @@ Examples:
1686
2003
  envVars[key] = value;
1687
2004
  });
1688
2005
  }
1689
- outputFileSync(
1690
- join6(process.cwd(), ".env.local"),
1691
- `${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
1692
- `
1693
- );
2006
+ writeEnv(process.cwd(), envVars);
1694
2007
  const label = channelName ? `"${channelName}" (${channelId})` : `${channelId}`;
1695
2008
  consola.success(
1696
- `Linked to channel ${label} and wrote ${colorize3("cyanBright", ".env.local")}.`
2009
+ `Linked to channel ${label} and wrote ${colorize5("cyanBright", ".env.local")}.`
1697
2010
  );
1698
2011
  consola.log(`Next steps:
1699
2012
 
1700
- ${colorize3("yellow", "pnpm run dev")}`);
2013
+ ${colorize5("yellow", "pnpm run dev")}`);
1701
2014
  process.exit(0);
1702
2015
  });
1703
- var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(link).addCommand(update);
2016
+ var create = new Command3("create").configureHelp({ showGlobalOptions: true }).description("Create a new Catalyst storefront channel on your BigCommerce store.").addHelpText(
2017
+ "after",
2018
+ `
2019
+ Examples:
2020
+ # Create a channel interactively (logs you in if needed)
2021
+ $ catalyst channel create
1704
2022
 
1705
- // src/cli/commands/create.ts
1706
- import { Command as Command4, InvalidArgumentError as InvalidArgumentError2, Option as Option5 } from "@commander-js/extra-typings";
1707
- import { checkbox, input as input3, select as select4 } from "@inquirer/prompts";
1708
- import { execSync as execSync4 } from "child_process";
1709
- import { colorize as colorize4 } from "consola/utils";
1710
- import { pathExistsSync } from "fs-extra/esm";
1711
- import kebabCase from "lodash.kebabcase";
1712
- import { join as join10 } from "path";
2023
+ # Non-interactive, and link it to this project afterwards
2024
+ $ catalyst channel create --name "My Store" --locale en --no-sample-data --link
1713
2025
 
1714
- // src/cli/lib/build-workspace-packages.ts
1715
- import { execSync } from "child_process";
1716
- import { existsSync as existsSync3 } from "fs";
1717
- import { join as join7 } from "path";
1718
- import yoctoSpinner2 from "yocto-spinner";
1719
- var WORKSPACE_PACKAGES = ["packages/catalyst", "packages/client"];
1720
- var buildWorkspacePackages = (projectDir) => {
1721
- const hasCore = existsSync3(join7(projectDir, "core"));
1722
- const hasAllWorkspacePackages = WORKSPACE_PACKAGES.every(
1723
- (pkg) => existsSync3(join7(projectDir, pkg))
2026
+ # Add additional storefront languages (max 4)
2027
+ $ catalyst channel create --name "My Store" --additional-locales es fr`
2028
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).option("--name <name>", "Name for the new channel. Skips the name prompt.").option(
2029
+ "--locale <locale>",
2030
+ 'Default storefront locale (e.g. "en"). Skips the default-language prompt.'
2031
+ ).option(
2032
+ "--additional-locales <locales...>",
2033
+ "Additional storefront locales, max 4 (e.g. --additional-locales es fr). Skips the additional-languages prompt."
2034
+ ).addOption(new Option3("--sample-data", "Install sample data on the new channel.")).addOption(new Option3("--no-sample-data", "Create the channel without sample data.")).option(
2035
+ "--link",
2036
+ "Link the new channel to this project \u2014 write its credentials to .env.local without prompting."
2037
+ ).addOption(
2038
+ new Option3("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2039
+ ).action(async (options) => {
2040
+ if (options.additionalLocales && options.additionalLocales.length > 4) {
2041
+ consola.error("You can only set up to 4 additional locales.");
2042
+ process.exit(1);
2043
+ return;
2044
+ }
2045
+ const config2 = getProjectConfig();
2046
+ const apiHost = resolveApiHost(options, config2);
2047
+ const credentials = await resolveCredentialsWithLogin(options, config2, apiHost);
2048
+ if (!credentials) {
2049
+ consola.info(
2050
+ "Login aborted. Re-run `catalyst channel create` when you have your credentials ready."
2051
+ );
2052
+ process.exit(0);
2053
+ return;
2054
+ }
2055
+ const { storeHash, accessToken } = credentials;
2056
+ await getTelemetry().identify(storeHash);
2057
+ const eligibility = await checkChannelEligibility(storeHash, accessToken, options.cliApiOrigin);
2058
+ if (!eligibility.eligible) {
2059
+ consola.warn(eligibility.message);
2060
+ process.exit(0);
2061
+ return;
2062
+ }
2063
+ const channelData = await runCreateChannelFlow({
2064
+ storeHash,
2065
+ accessToken,
2066
+ apiHost,
2067
+ cliApiOrigin: options.cliApiOrigin,
2068
+ name: options.name,
2069
+ locale: options.locale,
2070
+ additionalLocales: options.additionalLocales,
2071
+ sampleData: options.sampleData
2072
+ });
2073
+ consola.success(`Created channel ${channelData.channelId}.`);
2074
+ consola.warn(
2075
+ "A preview storefront has been deployed in your BigCommerce control panel. This preview may look different from your local environment as it may be running different code. Additionally, it may take a few minutes for the channel storefront to be accessible."
1724
2076
  );
1725
- if (!hasCore || !hasAllWorkspacePackages) return;
1726
- WORKSPACE_PACKAGES.forEach((pkg) => {
1727
- const spinner = yoctoSpinner2().start(`Building ${pkg}...`);
1728
- try {
1729
- execSync("pnpm build", { cwd: join7(projectDir, pkg), stdio: "ignore" });
1730
- spinner.success(`Built ${pkg}.`);
1731
- } catch (error) {
1732
- const message = error instanceof Error ? error.message : "unknown error";
1733
- spinner.error(`Failed to build ${pkg}: ${message}`);
1734
- throw error;
1735
- }
2077
+ const shouldLink = options.link ? true : await select4({
2078
+ message: "Would you like to link this channel to your project?",
2079
+ choices: [
2080
+ { name: "Yes", value: true },
2081
+ { name: "No", value: false }
2082
+ ]
1736
2083
  });
1737
- };
1738
-
1739
- // src/cli/lib/clone-catalyst.ts
1740
- import { execSync as execSync3 } from "child_process";
2084
+ if (shouldLink) {
2085
+ writeEnv(process.cwd(), channelData.envVars);
2086
+ consola.success(
2087
+ `Linked to channel ${channelData.channelId} and wrote ${colorize5("cyanBright", ".env.local")}.`
2088
+ );
2089
+ consola.log(`Next steps:
1741
2090
 
1742
- // src/cli/lib/checkout-ref.ts
1743
- import { sync as spawnSync } from "cross-spawn";
2091
+ ${colorize5("yellow", "pnpm run dev")}`);
2092
+ }
2093
+ process.exit(0);
2094
+ });
2095
+ var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(create).addCommand(link).addCommand(update);
1744
2096
 
1745
- // src/cli/lib/is-exec-exception.ts
1746
- function isExecException(error) {
1747
- return typeof error === "object" && error !== null && "stdout" in error && "stderr" in error;
1748
- }
2097
+ // src/cli/commands/create.ts
2098
+ import { Command as Command4, InvalidArgumentError as InvalidArgumentError3, Option as Option4 } from "@commander-js/extra-typings";
2099
+ import { input as input4, select as select5 } from "@inquirer/prompts";
2100
+ import { execSync as execSync2 } from "child_process";
2101
+ import { colorize as colorize6 } from "consola/utils";
2102
+ import { pathExistsSync } from "fs-extra/esm";
2103
+ import kebabCase from "lodash.kebabcase";
2104
+ import { join as join10 } from "path";
1749
2105
 
1750
- // src/cli/lib/checkout-ref.ts
1751
- function checkoutRef(repoDir, ref) {
1752
- try {
1753
- const spawn = spawnSync("git", ["checkout", ref, "--"], {
1754
- cwd: repoDir,
1755
- encoding: "utf8",
1756
- shell: false
1757
- });
1758
- const stderr = spawn.stderr.trim();
1759
- if (spawn.status !== 0 && stderr) {
1760
- throw new Error(stderr);
1761
- }
1762
- consola.success(`Checked out ref ${ref} successfully.`);
1763
- } catch (error) {
1764
- if (isExecException(error)) {
1765
- const stderr = error.stderr ? error.stderr.toString() : "";
1766
- 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`)) {
1767
- consola.error(`Ref '${ref}' not found in the repository.`);
1768
- } else {
1769
- consola.error(`Error checking out ref '${ref}':`, stderr.trim());
1770
- }
1771
- } else if (error instanceof Error) {
1772
- consola.error(`Error checking out ref '${ref}':`, error.message);
1773
- } else {
1774
- consola.error(`Unknown error occurred while checking out ref '${ref}'.`);
1775
- }
1776
- consola.warn(`Falling back to the default branch.`);
2106
+ // src/cli/lib/detect-package-manager.ts
2107
+ import { detectPackageManager as detectFromDir } from "nypm";
2108
+ var detectPackageManager = () => {
2109
+ const userAgent = process.env.npm_config_user_agent ?? "";
2110
+ const name = userAgent.split(" ")[0]?.split("/")[0];
2111
+ if (name === "pnpm" || name === "yarn" || name === "bun") {
2112
+ return name;
2113
+ }
2114
+ if (process.versions.bun) {
2115
+ return "bun";
2116
+ }
2117
+ return "npm";
2118
+ };
2119
+ var detectProjectPackageManager = async (projectDir) => {
2120
+ const detected = await detectFromDir(projectDir);
2121
+ const name = detected?.name;
2122
+ if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") {
2123
+ return name;
1777
2124
  }
1778
- }
2125
+ return "npm";
2126
+ };
1779
2127
 
1780
- // src/cli/lib/has-github-ssh.ts
1781
- import { execSync as execSync2 } from "child_process";
1782
- function hasGitHubSSH() {
2128
+ // src/cli/lib/extract-catalyst.ts
2129
+ import { mkdirSync as mkdirSync2 } from "fs";
2130
+ import { Readable } from "stream";
2131
+ import { pipeline } from "stream/promises";
2132
+ import { x } from "tar";
2133
+ var SUBDIR = "core";
2134
+ var MAX_ATTEMPTS = 3;
2135
+ var codeloadUrl = (repository, ref) => `https://codeload.github.com/${repository}/tar.gz/${encodeURIComponent(ref)}`;
2136
+ var wait = (ms) => new Promise((resolve2) => {
2137
+ setTimeout(resolve2, ms);
2138
+ });
2139
+ var downloadAndExtract = async (repository, ref, projectDir) => {
2140
+ const url = codeloadUrl(repository, ref);
2141
+ const response = await fetch(url);
2142
+ if (!response.ok || !response.body) {
2143
+ throw new Error(`Failed to download ${url} (HTTP ${response.status})`);
2144
+ }
2145
+ await pipeline(
2146
+ // Node's global fetch types `body` as the DOM ReadableStream; it is the same
2147
+ // object Readable.fromWeb expects at runtime.
2148
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
2149
+ Readable.fromWeb(response.body),
2150
+ x({
2151
+ cwd: projectDir,
2152
+ strip: 2,
2153
+ filter: (path) => path.split("/")[1] === SUBDIR
2154
+ })
2155
+ );
2156
+ };
2157
+ var attemptDownload = async (repository, ref, projectDir, attempt = 1) => {
1783
2158
  try {
1784
- const output = execSync2("ssh -T git@github.com", {
1785
- encoding: "utf8",
1786
- stdio: "pipe"
1787
- }).toString();
1788
- return output.includes("successfully authenticated");
2159
+ await downloadAndExtract(repository, ref, projectDir);
1789
2160
  } catch (error) {
1790
- if (isExecException(error)) {
1791
- const stdout = error.stdout ? error.stdout.toString() : "";
1792
- const stderr = error.stderr ? error.stderr.toString() : "";
1793
- const combinedOutput = stdout + stderr;
1794
- if (combinedOutput.includes("successfully authenticated")) {
1795
- return true;
1796
- }
2161
+ const message = error instanceof Error ? error.message : "unknown error";
2162
+ if (attempt >= MAX_ATTEMPTS) {
2163
+ throw new Error(`Failed to download Catalyst after ${MAX_ATTEMPTS} attempts: ${message}`);
1797
2164
  }
1798
- return false;
1799
- }
1800
- }
1801
-
1802
- // src/cli/lib/reset-branch-to-ref.ts
1803
- import { sync as spawnSync2 } from "cross-spawn";
1804
- function resetBranchToRef(projectDir, ghRef) {
1805
- const spawn = spawnSync2("git", ["reset", "--hard", ghRef, "--"], {
1806
- cwd: projectDir,
1807
- encoding: "utf8",
1808
- shell: false
1809
- });
1810
- const stderr = spawn.stderr.trim();
1811
- if (spawn.status !== 0 && stderr) {
1812
- throw new Error(stderr);
2165
+ consola.warn(`Download attempt ${attempt} failed (${message}). Retrying...`);
2166
+ await wait(attempt * 1e3);
2167
+ return attemptDownload(repository, ref, projectDir, attempt + 1);
1813
2168
  }
1814
- }
1815
-
1816
- // src/cli/lib/clone-catalyst.ts
1817
- var cloneCatalyst = ({
2169
+ };
2170
+ var extractCatalyst = async ({
1818
2171
  repository,
1819
- projectName,
1820
- projectDir,
1821
- ghRef,
1822
- resetMain = false
2172
+ ref,
2173
+ projectDir
1823
2174
  }) => {
1824
- const useSSH = hasGitHubSSH();
1825
- consola.info(`Cloning ${repository} using ${useSSH ? "SSH" : "HTTPS"}...`);
1826
- const cloneCommand = `git clone ${useSSH ? `git@github.com:${repository}` : `https://github.com/${repository}`}.git${projectName ? ` ${projectName}` : ""}`;
1827
- execSync3(cloneCommand, { stdio: "inherit" });
1828
- execSync3("git remote rename origin upstream", { cwd: projectDir, stdio: "inherit" });
1829
- if (ghRef) {
1830
- if (resetMain) {
1831
- execSync3("git checkout -b main", { cwd: projectDir, stdio: "inherit" });
1832
- resetBranchToRef(projectDir, ghRef);
1833
- consola.success(`Reset main to ${ghRef} successfully.`);
1834
- return;
1835
- }
1836
- checkoutRef(projectDir, ghRef);
2175
+ consola.info(`Downloading ${repository}#${ref}...`);
2176
+ mkdirSync2(projectDir, { recursive: true });
2177
+ await attemptDownload(repository, ref, projectDir);
2178
+ };
2179
+
2180
+ // src/cli/lib/init-git-repo.ts
2181
+ import { execSync } from "child_process";
2182
+ import { rmSync } from "fs";
2183
+ import { join as join7 } from "path";
2184
+ var run = (command, cwd) => execSync(command, { cwd, stdio: "ignore" });
2185
+ var initGitRepo = (projectDir) => {
2186
+ try {
2187
+ run("git init", projectDir);
2188
+ run("git add -A", projectDir);
2189
+ run('git commit -m "Initial commit from Catalyst"', projectDir);
2190
+ consola.success("Initialized a git repository.");
2191
+ } catch {
2192
+ rmSync(join7(projectDir, ".git"), { recursive: true, force: true });
2193
+ consola.warn("Could not initialize a git repository. Skipping.");
1837
2194
  }
1838
2195
  };
1839
2196
 
1840
2197
  // src/cli/lib/install-dependencies.ts
1841
2198
  import { installDependencies as installDeps } from "nypm";
1842
- import yoctoSpinner3 from "yocto-spinner";
1843
- var installDependencies = async (projectDir) => {
1844
- const spinner = yoctoSpinner3().start("Installing dependencies. This could take a minute...");
2199
+ import yoctoSpinner2 from "yocto-spinner";
2200
+ var restoreEnv = (key, previous) => {
2201
+ if (previous === void 0) {
2202
+ Reflect.deleteProperty(process.env, key);
2203
+ } else {
2204
+ process.env[key] = previous;
2205
+ }
2206
+ };
2207
+ var withQuietInstallEnv = async (run2) => {
2208
+ var _a, _b;
2209
+ const prevNodeNoWarnings = process.env.NODE_NO_WARNINGS;
2210
+ const prevDownloadPrompt = process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT;
2211
+ (_a = process.env).NODE_NO_WARNINGS ?? (_a.NODE_NO_WARNINGS = "1");
2212
+ (_b = process.env).COREPACK_ENABLE_DOWNLOAD_PROMPT ?? (_b.COREPACK_ENABLE_DOWNLOAD_PROMPT = "0");
2213
+ try {
2214
+ return await run2();
2215
+ } finally {
2216
+ restoreEnv("NODE_NO_WARNINGS", prevNodeNoWarnings);
2217
+ restoreEnv("COREPACK_ENABLE_DOWNLOAD_PROMPT", prevDownloadPrompt);
2218
+ }
2219
+ };
2220
+ var installDependencies = async (projectDir, packageManager = "pnpm") => {
2221
+ const spinner = yoctoSpinner2().start(
2222
+ `Installing dependencies with ${packageManager}. This could take a minute...`
2223
+ );
1845
2224
  try {
1846
- await installDeps({ cwd: projectDir, silent: true, packageManager: "pnpm" });
2225
+ await withQuietInstallEnv(() => installDeps({ cwd: projectDir, silent: true, packageManager }));
1847
2226
  spinner.success("Dependencies installed successfully.");
1848
2227
  } catch (error) {
1849
2228
  const message = error instanceof Error ? error.message : "unknown error";
@@ -1852,100 +2231,107 @@ var installDependencies = async (projectDir) => {
1852
2231
  }
1853
2232
  };
1854
2233
 
1855
- // src/cli/lib/localization.ts
1856
- import { z as z8 } from "zod";
1857
- var allowedLocales = [
1858
- "en",
1859
- "da",
1860
- "es-AR",
1861
- "es-CL",
1862
- "es-CO",
1863
- "es-MX",
1864
- "es-PE",
1865
- "es-419",
1866
- "es",
1867
- "it",
1868
- "nl",
1869
- "pl",
1870
- "pt",
1871
- "de",
1872
- "fr",
1873
- "ja",
1874
- "no",
1875
- "pt-BR",
1876
- "sv"
2234
+ // src/cli/lib/rewrite-core-package.ts
2235
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2236
+ import { join as join8 } from "path";
2237
+ import { z as z10 } from "zod";
2238
+ var NATIVE_BUILD_ALLOWLIST = [
2239
+ "@parcel/watcher",
2240
+ "@swc/core",
2241
+ "@vercel/speed-insights",
2242
+ "esbuild",
2243
+ "msw",
2244
+ "protobufjs",
2245
+ "puppeteer",
2246
+ "sharp",
2247
+ "workerd"
1877
2248
  ];
1878
- var AvailableLocalesSuccessSchema = z8.object({
1879
- data: z8.array(
1880
- z8.object({
1881
- id: z8.string(),
1882
- name: z8.string(),
1883
- fallback: z8.string().nullable(),
1884
- is_supported: z8.boolean()
1885
- })
1886
- )
2249
+ var DEP_FIELDS = [
2250
+ "dependencies",
2251
+ "devDependencies",
2252
+ "optionalDependencies",
2253
+ "peerDependencies"
2254
+ ];
2255
+ var corePackageJsonSchema2 = z10.looseObject({
2256
+ version: z10.string().optional(),
2257
+ private: z10.boolean().optional(),
2258
+ scripts: z10.record(z10.string(), z10.string()).optional(),
2259
+ dependencies: z10.record(z10.string(), z10.string()).optional(),
2260
+ devDependencies: z10.record(z10.string(), z10.string()).optional(),
2261
+ optionalDependencies: z10.record(z10.string(), z10.string()).optional(),
2262
+ peerDependencies: z10.record(z10.string(), z10.string()).optional(),
2263
+ pnpm: z10.looseObject({}).optional(),
2264
+ catalyst: z10.looseObject({}).optional()
1887
2265
  });
1888
- var getAvailableLocales = async (storeHash, accessToken, apiHost) => {
1889
- const response = await fetch(
1890
- `https://${apiHost}/stores/${storeHash}/v3/settings/store/available-locales`,
1891
- {
1892
- method: "GET",
1893
- headers: {
1894
- "X-Auth-Token": accessToken,
1895
- "X-Correlation-Id": getTelemetry().correlationId,
1896
- Accept: "application/json"
1897
- }
1898
- }
1899
- );
1900
- assertAuthorized(response);
2266
+ var registryVersionSchema = z10.object({ version: z10.string() });
2267
+ var resolvePublishedVersion = async (pkgName) => {
2268
+ const response = await fetch(`https://registry.npmjs.org/${pkgName}/latest`);
1901
2269
  if (!response.ok) {
1902
2270
  throw new Error(
1903
- `GET /v3/settings/store/available-locales failed: ${response.status} ${response.statusText}`
2271
+ `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.`
1904
2272
  );
1905
2273
  }
1906
- return AvailableLocalesSuccessSchema.parse(await response.json()).data.filter(({ id }) => allowedLocales.includes(id)).map(({ name, id }) => ({ name: `${name} (${id})`, value: id }));
2274
+ const { version: version2 } = registryVersionSchema.parse(await response.json());
2275
+ return `^${version2}`;
2276
+ };
2277
+ var rewriteCorePackage = async (projectDir, ref) => {
2278
+ const packageJsonPath = join8(projectDir, "package.json");
2279
+ const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync4(packageJsonPath, "utf-8")));
2280
+ const workspaceDeps = DEP_FIELDS.flatMap((field) => {
2281
+ const deps = pkg[field];
2282
+ if (!deps) return [];
2283
+ return Object.entries(deps).filter(([, range]) => range.startsWith("workspace:")).map(([name]) => ({ deps, name }));
2284
+ });
2285
+ await Promise.all(
2286
+ workspaceDeps.map(async ({ deps, name }) => {
2287
+ const resolved = await resolvePublishedVersion(name);
2288
+ deps[name] = resolved;
2289
+ consola.info(`Resolved ${name} to ${resolved}`);
2290
+ })
2291
+ );
2292
+ delete pkg.private;
2293
+ if (pkg.scripts) {
2294
+ pkg.scripts = Object.fromEntries(
2295
+ Object.entries(pkg.scripts).filter(([, command]) => !command.includes("turbo"))
2296
+ );
2297
+ }
2298
+ pkg.pnpm = { ...pkg.pnpm, onlyBuiltDependencies: NATIVE_BUILD_ALLOWLIST };
2299
+ pkg.trustedDependencies = NATIVE_BUILD_ALLOWLIST;
2300
+ pkg.catalyst = { ...pkg.catalyst, version: pkg.version, ref };
2301
+ writeFileSync2(packageJsonPath, `${JSON.stringify(sortPackageJsonFields(pkg), null, 2)}
2302
+ `);
2303
+ writeFileSync2(join8(projectDir, ".npmrc"), "legacy-peer-deps=true\n");
1907
2304
  };
1908
2305
 
1909
2306
  // src/cli/lib/setup-core-project.ts
1910
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1911
- import { dirname as dirname3, join as join8 } from "path";
1912
- import { z as z9 } from "zod";
1913
- var corePackageJsonSchema2 = z9.looseObject({
1914
- scripts: z9.record(z9.string(), z9.string()).optional(),
1915
- dependencies: z9.record(z9.string(), z9.string()).optional()
2307
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2308
+ import { dirname as dirname3, join as join9 } from "path";
2309
+ import { z as z11 } from "zod";
2310
+ var corePackageJsonSchema3 = z11.looseObject({
2311
+ scripts: z11.record(z11.string(), z11.string()).optional(),
2312
+ devDependencies: z11.record(z11.string(), z11.string()).optional()
1916
2313
  });
1917
2314
  var writeJson2 = (path, value) => {
1918
- mkdirSync2(dirname3(path), { recursive: true });
1919
- writeFileSync2(path, `${JSON.stringify(value, null, 2)}
2315
+ mkdirSync3(dirname3(path), { recursive: true });
2316
+ writeFileSync3(path, `${JSON.stringify(value, null, 2)}
1920
2317
  `);
1921
2318
  };
1922
2319
  var setupCoreProject = (projectDir) => {
1923
- const corePackageJsonPath = join8(projectDir, "core", "package.json");
1924
- const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync3(corePackageJsonPath, "utf-8")));
2320
+ const corePackageJsonPath = join9(projectDir, "package.json");
2321
+ const pkg = corePackageJsonSchema3.parse(JSON.parse(readFileSync5(corePackageJsonPath, "utf-8")));
1925
2322
  pkg.scripts = {
1926
2323
  ...pkg.scripts,
1927
2324
  build: "npm run generate && catalyst build",
1928
2325
  start: "catalyst start",
1929
2326
  deploy: "npm run generate && catalyst deploy"
1930
2327
  };
1931
- pkg.dependencies = {
1932
- ...pkg.dependencies,
2328
+ pkg.devDependencies = {
2329
+ ...pkg.devDependencies,
1933
2330
  "@bigcommerce/catalyst": package_default.version
1934
2331
  };
1935
2332
  writeJson2(corePackageJsonPath, sortPackageJsonFields(pkg));
1936
2333
  };
1937
2334
 
1938
- // src/cli/lib/write-env.ts
1939
- import { outputFileSync as outputFileSync2 } from "fs-extra/esm";
1940
- import { join as join9 } from "path";
1941
- var writeEnv = (projectDir, envVars) => {
1942
- outputFileSync2(
1943
- join9(projectDir, "core", ".env.local"),
1944
- `${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
1945
- `
1946
- );
1947
- };
1948
-
1949
2335
  // src/cli/commands/create.ts
1950
2336
  function getPlatformCheckCommand(command) {
1951
2337
  const isWindows = process.platform === "win32";
@@ -1954,73 +2340,25 @@ function getPlatformCheckCommand(command) {
1954
2340
  function parseChannelId2(value) {
1955
2341
  const parsed = parseInt(value, 10);
1956
2342
  if (Number.isNaN(parsed)) {
1957
- throw new InvalidArgumentError2(`"${value}" is not a valid channel ID (expected a number).`);
2343
+ throw new InvalidArgumentError3(`"${value}" is not a valid channel ID (expected a number).`);
1958
2344
  }
1959
2345
  return parsed;
1960
2346
  }
1961
2347
  function parseEnvFlag(value, previous = {}) {
1962
2348
  const eqIdx = value.indexOf("=");
1963
2349
  if (eqIdx <= 0) {
1964
- throw new InvalidArgumentError2(`Expected KEY=VALUE, got "${value}".`);
2350
+ throw new InvalidArgumentError3(`Expected KEY=VALUE, got "${value}".`);
1965
2351
  }
1966
2352
  const key = value.substring(0, eqIdx);
1967
2353
  const val = value.substring(eqIdx + 1);
1968
2354
  if (!val) {
1969
- throw new InvalidArgumentError2(`Expected KEY=VALUE with non-empty value, got "${value}".`);
2355
+ throw new InvalidArgumentError3(`Expected KEY=VALUE with non-empty value, got "${value}".`);
1970
2356
  }
1971
2357
  return { ...previous, [key]: val };
1972
2358
  }
1973
- async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrigin) {
1974
- const newChannelName = await input3({
1975
- message: "What would you like to name your new channel?"
1976
- });
1977
- const availableLocales = await getAvailableLocales(storeHash, accessToken, apiHost);
1978
- const storefrontLocale = await select4({
1979
- message: "Which default language would you like to set for your channel?",
1980
- default: "en",
1981
- choices: availableLocales,
1982
- theme: {
1983
- style: {
1984
- help: () => colorize4("dim", "(Select locale from the list or start typing the name)")
1985
- }
1986
- }
1987
- });
1988
- const shouldAddAdditionalLocales = await select4({
1989
- message: "Would you like to add additional languages?",
1990
- choices: [
1991
- { name: "Yes", value: true },
1992
- { name: "No", value: false }
1993
- ]
1994
- });
1995
- let additionalLocales = [];
1996
- if (shouldAddAdditionalLocales) {
1997
- const localeChoices = availableLocales.filter(({ value }) => value !== storefrontLocale).map(({ name, value }) => ({ name, value, description: value }));
1998
- additionalLocales = await checkbox({
1999
- message: "Which additional languages would you like to add to your channel?",
2000
- choices: localeChoices,
2001
- validate: (items) => items.length <= 4 || "You can only select up to 4 additional languages."
2002
- });
2003
- }
2004
- const shouldInstallSampleData = await select4({
2005
- message: "Would you like to install sample data?",
2006
- choices: [
2007
- { name: "Yes", value: true },
2008
- { name: "No", value: false }
2009
- ]
2010
- });
2011
- return createChannel(
2012
- newChannelName,
2013
- storefrontLocale,
2014
- additionalLocales,
2015
- shouldInstallSampleData,
2016
- storeHash,
2017
- accessToken,
2018
- cliApiOrigin
2019
- );
2020
- }
2021
2359
  async function handleChannelSelection(storeHash, accessToken, apiHost) {
2022
2360
  const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
2023
- const existingChannel = await select4({
2361
+ const existingChannel = await select5({
2024
2362
  message: "Which channel would you like to use?",
2025
2363
  choices: sortChannelsByPlatform(channels).map((ch) => ({
2026
2364
  name: ch.name,
@@ -2054,7 +2392,7 @@ async function setupProject(options) {
2054
2392
  projectDir = targetDir;
2055
2393
  return true;
2056
2394
  };
2057
- await input3({
2395
+ await input4({
2058
2396
  message: "What do you want to name your project directory?",
2059
2397
  default: "my-catalyst-app",
2060
2398
  validate: validateProjectName
@@ -2066,21 +2404,13 @@ async function setupProject(options) {
2066
2404
  }
2067
2405
  function checkRequiredTools() {
2068
2406
  try {
2069
- execSync4(getPlatformCheckCommand("git"), { stdio: "ignore" });
2407
+ execSync2(getPlatformCheckCommand("git"), { stdio: "ignore" });
2070
2408
  } catch {
2071
2409
  consola.error("git is required to create a Catalyst project");
2072
2410
  process.exit(1);
2073
2411
  }
2074
- try {
2075
- execSync4(getPlatformCheckCommand("pnpm"), { stdio: "ignore" });
2076
- } catch {
2077
- consola.error(
2078
- "pnpm is required to create a Catalyst project. Enable it by running `corepack enable pnpm`."
2079
- );
2080
- process.exit(1);
2081
- }
2082
2412
  }
2083
- var create = new Command4("create").configureHelp({ showGlobalOptions: true }).description("Scaffold and connect a Catalyst storefront to your BigCommerce store.").addHelpText(
2413
+ var create2 = new Command4("create").configureHelp({ showGlobalOptions: true }).description("Scaffold and connect a Catalyst storefront to your BigCommerce store.").addHelpText(
2084
2414
  "after",
2085
2415
  `
2086
2416
  Examples:
@@ -2094,14 +2424,14 @@ Examples:
2094
2424
  $ catalyst create --project-name my-store --hosting commerce`
2095
2425
  ).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(
2096
2426
  "--gh-ref <ref>",
2097
- "Clone a specific ref from the source repository",
2427
+ "Extract a specific ref (tag, branch, or commit) from the source repository",
2098
2428
  "@bigcommerce/catalyst-core@latest"
2099
- ).option("--reset-main", "Reset the main branch to the gh-ref").option("--repository <repository>", "GitHub repository to clone from", "bigcommerce/catalyst").option(
2429
+ ).option("--repository <repository>", "GitHub repository to extract from", "bigcommerce/catalyst").option(
2100
2430
  "--env <vars...>",
2101
2431
  "Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable).",
2102
2432
  parseEnvFlag
2103
2433
  ).addOption(
2104
- new Option5(
2434
+ new Option4(
2105
2435
  "--hosting <mode>",
2106
2436
  'Hosting mode: "self-hosted" (default) or "commerce" to set up Commerce Hosting at create time. When omitted, scaffolding is hosting-agnostic; run `catalyst deploy` later to opt in.'
2107
2437
  ).choices(["self-hosted", "commerce"])
@@ -2109,11 +2439,11 @@ Examples:
2109
2439
  "--use-existing",
2110
2440
  "Only used with --hosting commerce and --project-name. When the named project already exists on the store, reuse it instead of prompting. Has no effect without --hosting commerce."
2111
2441
  ).addOption(
2112
- new Option5("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
2442
+ new Option4("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
2113
2443
  ).addOption(
2114
- new Option5("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
2444
+ new Option4("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
2115
2445
  ).addOption(
2116
- new Option5("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2446
+ new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
2117
2447
  ).action(async (options) => {
2118
2448
  const { ghRef, repository } = options;
2119
2449
  if (options.useExisting && options.hosting !== "commerce") {
@@ -2167,7 +2497,7 @@ Examples:
2167
2497
  }
2168
2498
  let shouldCreateChannel;
2169
2499
  if (eligibility.eligible) {
2170
- shouldCreateChannel = await select4({
2500
+ shouldCreateChannel = await select5({
2171
2501
  message: "Would you like to create a new channel?",
2172
2502
  choices: [
2173
2503
  { name: "Yes", value: true },
@@ -2176,12 +2506,12 @@ Examples:
2176
2506
  });
2177
2507
  }
2178
2508
  if (shouldCreateChannel) {
2179
- const channelData = await handleChannelCreation(
2509
+ const channelData = await runCreateChannelFlow({
2180
2510
  storeHash,
2181
2511
  accessToken,
2182
2512
  apiHost,
2183
2513
  cliApiOrigin
2184
- );
2514
+ });
2185
2515
  channelId = channelData.channelId;
2186
2516
  storefrontToken = channelData.storefrontToken;
2187
2517
  envVars = { ...channelData.envVars };
@@ -2226,8 +2556,10 @@ Examples:
2226
2556
  );
2227
2557
  }
2228
2558
  consola.info(`Creating '${projectName}' at '${projectDir}'`);
2559
+ const packageManager = detectPackageManager();
2229
2560
  try {
2230
- cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
2561
+ await extractCatalyst({ repository, ref: ghRef, projectDir });
2562
+ await rewriteCorePackage(projectDir, ghRef);
2231
2563
  setupCoreProject(projectDir);
2232
2564
  if (useCommerceHosting && commerceHostingProject && storeHash && accessToken) {
2233
2565
  await setupCommerceHosting({
@@ -2238,8 +2570,8 @@ Examples:
2238
2570
  });
2239
2571
  }
2240
2572
  writeEnv(projectDir, envVars);
2241
- await installDependencies(projectDir);
2242
- buildWorkspacePackages(projectDir);
2573
+ await installDependencies(projectDir, packageManager);
2574
+ initGitRepo(projectDir);
2243
2575
  } catch (error) {
2244
2576
  if (pathExistsSync(projectDir)) {
2245
2577
  consola.warn(
@@ -2249,28 +2581,289 @@ Examples:
2249
2581
  throw error;
2250
2582
  }
2251
2583
  consola.success(`Created '${projectName}' at '${projectDir}'`);
2252
- const steps = [`cd ${projectName}/core && pnpm run dev`];
2584
+ const steps = [`cd ${projectName} && ${packageManager} run dev`];
2253
2585
  if (useCommerceHosting) {
2254
2586
  steps.push(
2255
- `Run 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce Hosting.`
2587
+ `Run 'cd ${projectName} && ${packageManager} run deploy' when ready to deploy to Commerce Hosting.`
2256
2588
  );
2257
2589
  }
2258
2590
  consola.log(
2259
2591
  `Next steps:
2260
2592
 
2261
- ${steps.map((step) => ` ${colorize4("yellow", step)}`).join("\n")}`
2593
+ ${steps.map((step) => ` ${colorize6("yellow", step)}`).join("\n")}`
2594
+ );
2595
+ });
2596
+
2597
+ // src/cli/commands/debug.ts
2598
+ import { Command as Command5, Option as Option5 } from "commander";
2599
+
2600
+ // src/cli/lib/collect-diagnostics.ts
2601
+ import { parse } from "dotenv";
2602
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2603
+ import { release } from "os";
2604
+ import { dirname as dirname4, join as join11 } from "path";
2605
+ import { z as z12 } from "zod";
2606
+ var CLI_ENV_VARS = [
2607
+ "CATALYST_STORE_HASH",
2608
+ "CATALYST_ACCESS_TOKEN",
2609
+ "CATALYST_PROJECT_UUID",
2610
+ "CATALYST_API_HOST",
2611
+ "BIGCOMMERCE_LOGIN_URL",
2612
+ "CATALYST_TELEMETRY_DISABLED"
2613
+ ];
2614
+ var BUILD_ENV_VARS = [
2615
+ "BIGCOMMERCE_STORE_HASH",
2616
+ "BIGCOMMERCE_STOREFRONT_TOKEN",
2617
+ "BIGCOMMERCE_CHANNEL_ID",
2618
+ "AUTH_SECRET"
2619
+ ];
2620
+ var ENV_FILES = [".env.local", ".env"];
2621
+ var PROJECT_LOCKFILES = [
2622
+ ["pnpm-lock.yaml", "pnpm"],
2623
+ ["yarn.lock", "yarn"],
2624
+ ["bun.lock", "bun"],
2625
+ ["bun.lockb", "bun"],
2626
+ ["package-lock.json", "npm"]
2627
+ ];
2628
+ var hasValue = (value) => value !== void 0 && value.trim() !== "";
2629
+ var buildEnvLayers = (cwd, env2) => {
2630
+ const layers = [{ source: "process.env", values: env2 }];
2631
+ ENV_FILES.forEach((file) => {
2632
+ const path = join11(cwd, file);
2633
+ if (!existsSync5(path)) {
2634
+ return;
2635
+ }
2636
+ try {
2637
+ layers.push({ source: file, values: parse(readFileSync6(path, "utf-8")) });
2638
+ } catch {
2639
+ }
2640
+ });
2641
+ return layers;
2642
+ };
2643
+ var resolveEnvSource = (layers, ...keys) => layers.find((layer) => keys.some((key) => hasValue(layer.values[key])))?.source;
2644
+ var resolveEnvVars = (layers, names) => Object.fromEntries(names.map((name) => [name, resolveEnvSource(layers, name) ?? "unset"]));
2645
+ var resolveValue = (envSource, configValue) => {
2646
+ if (envSource) {
2647
+ return { present: true, source: envSource };
2648
+ }
2649
+ if (hasValue(configValue)) {
2650
+ return { present: true, source: "project.json" };
2651
+ }
2652
+ return { present: false, source: "unset" };
2653
+ };
2654
+ var asString = (value) => typeof value === "string" ? value : void 0;
2655
+ var lockfileManager = (dir) => PROJECT_LOCKFILES.find(([file]) => existsSync5(join11(dir, file)))?.[1];
2656
+ var detectProjectPackageManager2 = (cwd) => {
2657
+ let dir = cwd;
2658
+ for (; ; ) {
2659
+ const match = lockfileManager(dir);
2660
+ if (match) {
2661
+ return match;
2662
+ }
2663
+ const parent = dirname4(dir);
2664
+ if (parent === dir) {
2665
+ return detectPackageManager();
2666
+ }
2667
+ dir = parent;
2668
+ }
2669
+ };
2670
+ var projectJsonSchema2 = z12.looseObject({});
2671
+ var safeReadJson2 = (path) => {
2672
+ try {
2673
+ return JSON.parse(readFileSync6(path, "utf-8"));
2674
+ } catch {
2675
+ return null;
2676
+ }
2677
+ };
2678
+ var readProjectJson = (cwd) => {
2679
+ const result = projectJsonSchema2.safeParse(
2680
+ safeReadJson2(join11(cwd, ".bigcommerce", "project.json"))
2262
2681
  );
2682
+ return result.success ? result.data : null;
2683
+ };
2684
+ var storefrontPackageSchema = z12.looseObject({
2685
+ name: z12.string().optional(),
2686
+ version: z12.string().optional(),
2687
+ catalyst: z12.looseObject({ version: z12.string().optional() }).optional()
2688
+ });
2689
+ var readStorefrontInfo = (cwd) => {
2690
+ const result = storefrontPackageSchema.safeParse(safeReadJson2(join11(cwd, "package.json")));
2691
+ if (!result.success) {
2692
+ return { name: null, version: null };
2693
+ }
2694
+ return {
2695
+ name: result.data.name ?? null,
2696
+ version: result.data.catalyst?.version ?? result.data.version ?? null
2697
+ };
2698
+ };
2699
+ var readStoredEnvKeys = (projectJson) => {
2700
+ const env2 = projectJson?.env;
2701
+ if (typeof env2 === "object" && env2 !== null && !Array.isArray(env2)) {
2702
+ return Object.keys(env2).sort();
2703
+ }
2704
+ return [];
2705
+ };
2706
+ function collectDiagnostics({
2707
+ cwd = process.cwd(),
2708
+ env: env2 = process.env
2709
+ } = {}) {
2710
+ const state = getProjectState(cwd);
2711
+ const telemetry3 = getTelemetry();
2712
+ const projectJson = readProjectJson(cwd);
2713
+ const storefront = readStorefrontInfo(cwd);
2714
+ const cliEnv = [{ source: "process.env", values: env2 }];
2715
+ const buildEnv = buildEnvLayers(cwd, env2);
2716
+ return {
2717
+ cli: {
2718
+ name: package_default.name,
2719
+ version: package_default.version
2720
+ },
2721
+ runtime: {
2722
+ node: process.version,
2723
+ platform: process.platform,
2724
+ arch: process.arch,
2725
+ osRelease: release(),
2726
+ packageManager: detectProjectPackageManager2(cwd)
2727
+ },
2728
+ project: {
2729
+ cwd,
2730
+ storefrontName: storefront.name,
2731
+ storefrontVersion: storefront.version,
2732
+ projectUuid: state.projectUuid ?? null,
2733
+ isLinked: state.isLinked,
2734
+ isTransformed: state.isTransformed,
2735
+ isFullySetUp: state.isFullySetUp,
2736
+ hasMiddleware: state.hasMiddleware,
2737
+ hasProxy: state.hasProxy,
2738
+ hasOpenNextDep: state.hasOpenNextDep
2739
+ },
2740
+ config: {
2741
+ storeHash: resolveValue(
2742
+ resolveEnvSource(cliEnv, "CATALYST_STORE_HASH", "BIGCOMMERCE_STORE_HASH"),
2743
+ asString(projectJson?.storeHash)
2744
+ ),
2745
+ accessToken: resolveValue(
2746
+ resolveEnvSource(cliEnv, "CATALYST_ACCESS_TOKEN"),
2747
+ asString(projectJson?.accessToken)
2748
+ ),
2749
+ projectUuid: resolveValue(
2750
+ resolveEnvSource(cliEnv, "CATALYST_PROJECT_UUID"),
2751
+ asString(projectJson?.projectUuid)
2752
+ ),
2753
+ apiHost: resolveValue(
2754
+ resolveEnvSource(cliEnv, "CATALYST_API_HOST"),
2755
+ asString(projectJson?.apiHost)
2756
+ ),
2757
+ projectJsonKeys: projectJson ? Object.keys(projectJson).sort() : [],
2758
+ storedEnvKeys: readStoredEnvKeys(projectJson),
2759
+ cliEnvVars: resolveEnvVars(cliEnv, CLI_ENV_VARS),
2760
+ buildEnvVars: resolveEnvVars(buildEnv, BUILD_ENV_VARS)
2761
+ },
2762
+ telemetry: {
2763
+ enabled: telemetry3.isEnabled(),
2764
+ correlationId: telemetry3.correlationId
2765
+ },
2766
+ files: {
2767
+ ".env.local": existsSync5(join11(cwd, ".env.local")),
2768
+ ".env": existsSync5(join11(cwd, ".env")),
2769
+ ".bigcommerce/project.json": existsSync5(join11(cwd, ".bigcommerce", "project.json")),
2770
+ ".bigcommerce/wrangler.jsonc": existsSync5(join11(cwd, ".bigcommerce", "wrangler.jsonc")),
2771
+ ".open-next/": existsSync5(join11(cwd, ".open-next")),
2772
+ "package.json": existsSync5(join11(cwd, "package.json"))
2773
+ }
2774
+ };
2775
+ }
2776
+
2777
+ // src/cli/commands/debug.ts
2778
+ var yesNo = (value) => value ? "yes" : "no";
2779
+ var presence = (value) => value ? "present" : "absent";
2780
+ var formatResolved = (value) => value.present ? `present (source: ${value.source})` : "not set";
2781
+ var formatSource = (source) => source === "unset" ? "not set" : `set (${source})`;
2782
+ var formatStorefront = (name, version2) => {
2783
+ if (!version2) {
2784
+ return "(unknown)";
2785
+ }
2786
+ return name ? `${name}@${version2}` : version2;
2787
+ };
2788
+ var formatList = (values) => values.length > 0 ? values.join(", ") : "(none)";
2789
+ var envVarLines = (vars) => Object.entries(vars).map(([name, source]) => ` ${name}: ${formatSource(source)}`);
2790
+ var formatReport = (d) => {
2791
+ const lines = [
2792
+ "Catalyst CLI Diagnostics",
2793
+ "",
2794
+ "CLI",
2795
+ ` Package: ${d.cli.name}`,
2796
+ ` Version: ${d.cli.version}`,
2797
+ "",
2798
+ "Runtime",
2799
+ ` Node: ${d.runtime.node}`,
2800
+ ` Platform: ${d.runtime.platform} (${d.runtime.arch})`,
2801
+ ` OS release: ${d.runtime.osRelease}`,
2802
+ ` Package manager: ${d.runtime.packageManager}`,
2803
+ "",
2804
+ "Project",
2805
+ ` Directory: ${d.project.cwd}`,
2806
+ ` Storefront: ${formatStorefront(d.project.storefrontName, d.project.storefrontVersion)}`,
2807
+ ` Project UUID: ${d.project.projectUuid ?? "(not linked)"}`,
2808
+ ` Linked: ${yesNo(d.project.isLinked)}`,
2809
+ ` Transformed: ${yesNo(d.project.isTransformed)}`,
2810
+ ` Fully set up: ${yesNo(d.project.isFullySetUp)}`,
2811
+ ` middleware.ts: ${presence(d.project.hasMiddleware)}`,
2812
+ ` proxy.ts: ${presence(d.project.hasProxy)}`,
2813
+ ` OpenNext dep: ${d.project.hasOpenNextDep ? "installed" : "not installed"}`,
2814
+ "",
2815
+ "Config (resolved without secrets)",
2816
+ ` Store hash: ${formatResolved(d.config.storeHash)}`,
2817
+ ` Access token: ${formatResolved(d.config.accessToken)}`,
2818
+ ` Project UUID: ${formatResolved(d.config.projectUuid)}`,
2819
+ ` API host: ${d.config.apiHost.present ? formatResolved(d.config.apiHost) : "default (api.bigcommerce.com)"}`,
2820
+ ` project.json keys: ${formatList(d.config.projectJsonKeys)}`,
2821
+ ` Stored env keys: ${formatList(d.config.storedEnvKeys)}`,
2822
+ "",
2823
+ "CLI environment variables (used to run the CLI)",
2824
+ ...envVarLines(d.config.cliEnvVars),
2825
+ "",
2826
+ "Build environment variables (used to build the Next.js app)",
2827
+ ...envVarLines(d.config.buildEnvVars),
2828
+ "",
2829
+ "Telemetry",
2830
+ ` Enabled: ${yesNo(d.telemetry.enabled)}`,
2831
+ ` Correlation ID: ${d.telemetry.correlationId}`,
2832
+ "",
2833
+ "Files",
2834
+ ...Object.entries(d.files).map(([name, exists]) => ` ${name}: ${presence(exists)}`)
2835
+ ];
2836
+ return lines.join("\n");
2837
+ };
2838
+ var debug = new Command5("debug").configureHelp({ showGlobalOptions: true }).description(
2839
+ "Print a diagnostic report (CLI, runtime, project, and config state) to include when filing a bug report. Never prints secret values \u2014 credentials and env vars are reported by presence only."
2840
+ ).addHelpText(
2841
+ "after",
2842
+ `
2843
+ Examples:
2844
+ # Print a human-readable report
2845
+ $ catalyst debug
2846
+
2847
+ # Print machine-readable JSON (useful for copy/paste or piping)
2848
+ $ catalyst debug --json`
2849
+ ).addOption(new Option5("--json", "Output the report as JSON.")).action((options) => {
2850
+ const diagnostics = collectDiagnostics();
2851
+ if (options.json) {
2852
+ consola.log(JSON.stringify(diagnostics, null, 2));
2853
+ return;
2854
+ }
2855
+ consola.log(formatReport(diagnostics));
2263
2856
  });
2264
2857
 
2265
2858
  // src/cli/commands/deploy.ts
2266
2859
  import { confirm as confirm3 } from "@inquirer/prompts";
2267
2860
  import AdmZip from "adm-zip";
2268
- import { Command as Command5, Option as Option6 } from "commander";
2269
- import { colorize as colorize5 } from "consola/utils";
2861
+ import { Command as Command6, Option as Option6 } from "commander";
2862
+ import { colorize as colorize7 } from "consola/utils";
2270
2863
  import { access, readdir, readFile } from "fs/promises";
2271
- import { dirname as dirname4, join as join11 } from "path";
2272
- import yoctoSpinner4 from "yocto-spinner";
2273
- import { z as z10 } from "zod";
2864
+ import { join as join12 } from "path";
2865
+ import yoctoSpinner3 from "yocto-spinner";
2866
+ import { z as z13 } from "zod";
2274
2867
 
2275
2868
  // src/cli/lib/deployment-errors.ts
2276
2869
  var ERROR_MESSAGES = {
@@ -2289,7 +2882,7 @@ function getDeploymentErrorMessage(code) {
2289
2882
  }
2290
2883
 
2291
2884
  // src/cli/commands/deploy.ts
2292
- var stepsEnum = z10.enum([
2885
+ var stepsEnum = z13.enum([
2293
2886
  "initializing",
2294
2887
  "downloading",
2295
2888
  "unzipping",
@@ -2307,41 +2900,41 @@ var STEPS = {
2307
2900
  finalizing: "Finalizing...",
2308
2901
  complete: "Complete"
2309
2902
  };
2310
- var UploadSignatureSchema = z10.object({
2311
- data: z10.object({
2312
- upload_url: z10.url(),
2313
- upload_uuid: z10.string()
2903
+ var UploadSignatureSchema = z13.object({
2904
+ data: z13.object({
2905
+ upload_url: z13.url(),
2906
+ upload_uuid: z13.string()
2314
2907
  })
2315
2908
  });
2316
- var CreateDeploymentSchema = z10.object({
2317
- data: z10.object({
2318
- deployment_uuid: z10.uuid()
2909
+ var CreateDeploymentSchema = z13.object({
2910
+ data: z13.object({
2911
+ deployment_uuid: z13.uuid()
2319
2912
  })
2320
2913
  });
2321
- var ProjectSchema = z10.object({
2322
- data: z10.array(
2323
- z10.object({
2324
- uuid: z10.uuid(),
2325
- name: z10.string()
2914
+ var ProjectSchema = z13.object({
2915
+ data: z13.array(
2916
+ z13.object({
2917
+ uuid: z13.uuid(),
2918
+ name: z13.string()
2326
2919
  })
2327
2920
  )
2328
2921
  });
2329
- var DeploymentStatusSchema = z10.object({
2330
- deployment_uuid: z10.uuid(),
2331
- deployment_status: z10.enum(["queued", "in_progress", "failed", "completed"]),
2332
- event: z10.object({
2922
+ var DeploymentStatusSchema = z13.object({
2923
+ deployment_uuid: z13.uuid(),
2924
+ deployment_status: z13.enum(["queued", "in_progress", "failed", "completed"]),
2925
+ event: z13.object({
2333
2926
  step: stepsEnum,
2334
- progress: z10.number()
2927
+ progress: z13.number()
2335
2928
  }).nullable(),
2336
- deployment_hostnames: z10.array(z10.string()).optional(),
2337
- error: z10.object({
2338
- code: z10.number()
2929
+ deployment_hostnames: z13.array(z13.string()).optional(),
2930
+ error: z13.object({
2931
+ code: z13.number()
2339
2932
  }).optional()
2340
2933
  });
2341
2934
  var generateBundleZip = async () => {
2342
2935
  consola.info("Generating bundle...");
2343
- const bigcommerceDir = join11(process.cwd(), ".bigcommerce");
2344
- const distDir = join11(process.cwd(), ".bigcommerce", "dist");
2936
+ const bigcommerceDir = join12(process.cwd(), ".bigcommerce");
2937
+ const distDir = join12(process.cwd(), ".bigcommerce", "dist");
2345
2938
  try {
2346
2939
  await access(distDir);
2347
2940
  } catch {
@@ -2351,7 +2944,7 @@ var generateBundleZip = async () => {
2351
2944
  if (buildDirContents.length === 0) {
2352
2945
  throw new Error(`Dist directory is empty: ${distDir}`);
2353
2946
  }
2354
- const outputZip = join11(bigcommerceDir, "bundle.zip");
2947
+ const outputZip = join12(bigcommerceDir, "bundle.zip");
2355
2948
  const zip = new AdmZip();
2356
2949
  zip.addLocalFolder(distDir, "output");
2357
2950
  zip.writeZip(outputZip);
@@ -2374,7 +2967,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
2374
2967
  );
2375
2968
  assertAuthorized(response);
2376
2969
  if (!response.ok) {
2377
- throw new Error(`Failed to fetch upload signature: ${response.status} ${response.statusText}`);
2970
+ throw await httpError(response, "Failed to generate upload signature");
2378
2971
  }
2379
2972
  const res = await response.json();
2380
2973
  const { data } = UploadSignatureSchema.parse(res);
@@ -2383,7 +2976,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
2383
2976
  };
2384
2977
  var uploadBundleZip = async (uploadUrl) => {
2385
2978
  consola.info("Uploading bundle...");
2386
- const zipPath = join11(process.cwd(), ".bigcommerce", "bundle.zip");
2979
+ const zipPath = join12(process.cwd(), ".bigcommerce", "bundle.zip");
2387
2980
  const fileBuffer = await readFile(zipPath);
2388
2981
  const response = await fetch(uploadUrl, {
2389
2982
  method: "PUT",
@@ -2393,7 +2986,7 @@ var uploadBundleZip = async (uploadUrl) => {
2393
2986
  body: fileBuffer
2394
2987
  });
2395
2988
  if (!response.ok) {
2396
- throw new Error(`Failed to upload bundle: ${response.status} ${response.statusText}`);
2989
+ throw await httpError(response, "Failed to upload bundle");
2397
2990
  }
2398
2991
  consola.success("Bundle uploaded successfully.");
2399
2992
  return true;
@@ -2429,7 +3022,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
2429
3022
  );
2430
3023
  assertAuthorized(response);
2431
3024
  if (!response.ok) {
2432
- throw new Error(`Failed to create deployment: ${response.status} ${response.statusText}`);
3025
+ throw await httpError(response, "Failed to create deployment");
2433
3026
  }
2434
3027
  const res = await response.json();
2435
3028
  const { data } = CreateDeploymentSchema.parse(res);
@@ -2438,7 +3031,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
2438
3031
  };
2439
3032
  var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost) => {
2440
3033
  consola.info("Fetching deployment status...");
2441
- const spinner = yoctoSpinner4().start("Fetching...");
3034
+ const spinner = yoctoSpinner3().start("Fetching...");
2442
3035
  const response = await fetch(
2443
3036
  `https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments/${deploymentUuid}/events`,
2444
3037
  {
@@ -2453,7 +3046,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
2453
3046
  );
2454
3047
  assertAuthorized(response);
2455
3048
  if (!response.ok) {
2456
- throw new Error(`Failed to open event stream: ${response.status} ${response.statusText}`);
3049
+ throw await httpError(response, "Failed to open deployment event stream");
2457
3050
  }
2458
3051
  const reader = response.body?.getReader();
2459
3052
  if (!reader) {
@@ -2494,7 +3087,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
2494
3087
  spinner.success("Deployment completed successfully.");
2495
3088
  if (deploymentHostname) {
2496
3089
  consola.success(
2497
- `View your deployment at: ${colorize5("blue", `https://${deploymentHostname}`)}`
3090
+ `View your deployment at: ${colorize7("blue", `https://${deploymentHostname}`)}`
2498
3091
  );
2499
3092
  }
2500
3093
  return deploymentHostname;
@@ -2514,13 +3107,13 @@ var fetchProject = async (projectUuid, storeHash, accessToken, apiHost) => {
2514
3107
  );
2515
3108
  assertAuthorized(response);
2516
3109
  if (!response.ok) {
2517
- throw new Error(`Failed to fetch projects: ${response.status} ${response.statusText}`);
3110
+ throw await httpError(response, "Failed to fetch projects");
2518
3111
  }
2519
3112
  const res = await response.json();
2520
3113
  const { data } = ProjectSchema.parse(res);
2521
3114
  return data.find((project2) => project2.uuid === projectUuid);
2522
3115
  };
2523
- var deploy = new Command5("deploy").configureHelp({ showGlobalOptions: true }).description("Deploy your application to Cloudflare.").addHelpText(
3116
+ var deploy = new Command6("deploy").configureHelp({ showGlobalOptions: true }).description("Deploy your application to Cloudflare.").addHelpText(
2524
3117
  "after",
2525
3118
  `
2526
3119
  Environment variables saved with \`catalyst env add\` are sent automatically on every deploy.
@@ -2541,8 +3134,14 @@ Example:
2541
3134
  ).option("--dry-run", "Run the command to generate the bundle without uploading or deploying.").option(
2542
3135
  "--prebuilt",
2543
3136
  "Skip the build step. Requires .bigcommerce/dist/ to already contain build output."
2544
- ).action(async (options) => {
3137
+ ).addOption(
3138
+ new Option6(
3139
+ "--wrangler-version <version>",
3140
+ `Wrangler version or dist-tag to build with. Ignored with --prebuilt. Defaults to ${WRANGLER_VERSION}.`
3141
+ ).argParser(parseWranglerVersion)
3142
+ ).addOption(envPathOption()).action(async (options) => {
2545
3143
  const config2 = getProjectConfig();
3144
+ const apiHost = resolveApiHost(options, config2);
2546
3145
  const { storeHash, accessToken } = resolveCredentials(options, config2);
2547
3146
  const telemetry3 = getTelemetry();
2548
3147
  await telemetry3.identify(storeHash);
@@ -2553,7 +3152,7 @@ Example:
2553
3152
  return await selectOrCreateInfrastructureProject({
2554
3153
  storeHash,
2555
3154
  accessToken,
2556
- apiHost: options.apiHost
3155
+ apiHost
2557
3156
  });
2558
3157
  } catch (error) {
2559
3158
  if (error instanceof NoLinkedProjectError) {
@@ -2566,12 +3165,7 @@ Example:
2566
3165
  }
2567
3166
  };
2568
3167
  if (linkedProjectUuid) {
2569
- const existing = await fetchProject(
2570
- linkedProjectUuid,
2571
- storeHash,
2572
- accessToken,
2573
- options.apiHost
2574
- );
3168
+ const existing = await fetchProject(linkedProjectUuid, storeHash, accessToken, apiHost);
2575
3169
  if (existing) {
2576
3170
  projectUuid = linkedProjectUuid;
2577
3171
  } else {
@@ -2599,15 +3193,16 @@ Example:
2599
3193
  consola.info("When you're ready to deploy, re-run `catalyst deploy` to complete setup.");
2600
3194
  process.exit(0);
2601
3195
  }
2602
- const projectDir = dirname4(process.cwd());
3196
+ const projectDir = process.cwd();
2603
3197
  await setupCommerceHosting({ projectDir, projectUuid, storeHash, accessToken });
2604
3198
  consola.success("Commerce Hosting setup complete.");
2605
- await installDependencies(projectDir);
3199
+ const packageManager = await detectProjectPackageManager(projectDir);
3200
+ await installDependencies(projectDir, packageManager);
2606
3201
  } else {
2607
- await cleanupCloudflareIncompatibilities(dirname4(process.cwd()));
3202
+ await cleanupCloudflareIncompatibilities(process.cwd());
2608
3203
  }
2609
3204
  if (options.prebuilt) {
2610
- const distDir = join11(process.cwd(), ".bigcommerce", "dist");
3205
+ const distDir = join12(process.cwd(), ".bigcommerce", "dist");
2611
3206
  try {
2612
3207
  await access(distDir);
2613
3208
  } catch {
@@ -2623,7 +3218,8 @@ Example:
2623
3218
  }
2624
3219
  consola.info("Using existing build output (--prebuilt).");
2625
3220
  } else {
2626
- await buildCatalystProject(projectUuid);
3221
+ loadBuildEnv({ envPath: options.envPath });
3222
+ await buildCatalystProject(projectUuid, options.wranglerVersion);
2627
3223
  }
2628
3224
  await generateBundleZip();
2629
3225
  if (options.dryRun) {
@@ -2634,7 +3230,7 @@ Example:
2634
3230
  consola.info("- Create deployment");
2635
3231
  process.exit(0);
2636
3232
  }
2637
- const uploadSignature = await generateUploadSignature(storeHash, accessToken, options.apiHost);
3233
+ const uploadSignature = await generateUploadSignature(storeHash, accessToken, apiHost);
2638
3234
  await uploadBundleZip(uploadSignature.upload_url);
2639
3235
  const flagSecrets = parseEnvironmentVariables(options.secret) ?? [];
2640
3236
  const persistedSecrets = toDeploymentSecrets(getStoredEnv(config2));
@@ -2645,40 +3241,772 @@ Example:
2645
3241
  uploadSignature.upload_uuid,
2646
3242
  storeHash,
2647
3243
  accessToken,
2648
- options.apiHost,
3244
+ apiHost,
2649
3245
  environmentVariables
2650
3246
  );
2651
3247
  const deploymentHostname = await getDeploymentStatus(
2652
3248
  deploymentUuid,
2653
3249
  storeHash,
2654
3250
  accessToken,
2655
- options.apiHost
3251
+ apiHost
3252
+ );
3253
+ if (!options.updateSiteUrl) {
3254
+ return;
3255
+ }
3256
+ try {
3257
+ await runChannelSiteUrlFlow({
3258
+ storeHash,
3259
+ accessToken,
3260
+ apiHost,
3261
+ projectUuid,
3262
+ preferHostname: deploymentHostname
3263
+ });
3264
+ } catch (error) {
3265
+ consola.warn(
3266
+ `Failed to update channel site URL: ${error instanceof Error ? error.message : String(error)}`
3267
+ );
3268
+ consola.info(
3269
+ "Update it manually in the control panel, or re-run `catalyst auth login` if the token is missing the store_channel_settings scope."
3270
+ );
3271
+ }
3272
+ });
3273
+
3274
+ // src/cli/commands/domains.ts
3275
+ import { confirm as confirm4, select as select6 } from "@inquirer/prompts";
3276
+ import { Command as Command7, Option as Option7 } from "commander";
3277
+ import { colorize as colorize9 } from "consola/utils";
3278
+
3279
+ // src/cli/lib/domains.ts
3280
+ import { z as z15 } from "zod";
3281
+
3282
+ // src/cli/lib/observability.ts
3283
+ import { colorize as colorize8 } from "consola/utils";
3284
+ import { z as z14 } from "zod";
3285
+ var LOG_LEVELS = ["debug", "info", "warn", "error"];
3286
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
3287
+ var logEntrySchema = z14.object({
3288
+ timestamp: z14.string().nullable().optional(),
3289
+ level: z14.string().optional(),
3290
+ messages: z14.array(z14.unknown()).optional(),
3291
+ is_exception: z14.boolean().optional(),
3292
+ exception_name: z14.string().optional(),
3293
+ request: z14.object({
3294
+ method: z14.string().optional(),
3295
+ url: z14.string().optional(),
3296
+ status_code: z14.number().optional()
3297
+ }).nullable().optional()
3298
+ });
3299
+ var queryLogsSchema = z14.object({
3300
+ data: z14.array(logEntrySchema)
3301
+ });
3302
+ var v3ErrorSchema = z14.object({
3303
+ title: z14.string().optional(),
3304
+ errors: z14.record(z14.string(), z14.string()).optional()
3305
+ });
3306
+ function formatV3Error(body) {
3307
+ const parsed = v3ErrorSchema.safeParse(body);
3308
+ if (!parsed.success) return void 0;
3309
+ const { title, errors } = parsed.data;
3310
+ const fieldErrors = errors && Object.keys(errors).length > 0 ? Object.entries(errors).map(([field, message]) => `${field}: ${message}`).join("; ") : void 0;
3311
+ if (title && fieldErrors) return `${title} (${fieldErrors})`;
3312
+ return title ?? fieldErrors;
3313
+ }
3314
+ function parseTimeInput(value) {
3315
+ const trimmed = value.trim();
3316
+ if (/^\d+$/.test(trimmed)) {
3317
+ return Number(trimmed) * 1e3;
3318
+ }
3319
+ const ms = Date.parse(trimmed);
3320
+ return Number.isNaN(ms) ? null : ms;
3321
+ }
3322
+ var DURATION_UNIT_MS = {
3323
+ s: 1e3,
3324
+ m: 60 * 1e3,
3325
+ h: 60 * 60 * 1e3,
3326
+ d: 24 * 60 * 60 * 1e3
3327
+ };
3328
+ function parseDuration(value) {
3329
+ const match = /^(\d+)([smhd])$/.exec(value.trim());
3330
+ if (!match) return null;
3331
+ return Number(match[1]) * DURATION_UNIT_MS[match[2]];
3332
+ }
3333
+ function resolveTimeWindow(opts, nowMs = Date.now()) {
3334
+ const { since } = opts;
3335
+ let { start: start2, end } = opts;
3336
+ let startMs;
3337
+ let endMs;
3338
+ if (end) {
3339
+ endMs = parseTimeInput(end);
3340
+ if (endMs === null) {
3341
+ throw new UserActionableError(
3342
+ `Invalid --end value "${end}". Provide an ISO-8601 timestamp or a Unix epoch (seconds).`
3343
+ );
3344
+ }
3345
+ } else {
3346
+ endMs = nowMs;
3347
+ end = new Date(endMs).toISOString();
3348
+ }
3349
+ if (since) {
3350
+ const durationMs = parseDuration(since);
3351
+ if (durationMs === null) {
3352
+ throw new UserActionableError(
3353
+ `Invalid --since value "${since}". Provide a duration like 30m, 6h, or 2d (units: s, m, h, d).`
3354
+ );
3355
+ }
3356
+ startMs = endMs - durationMs;
3357
+ start2 = new Date(startMs).toISOString();
3358
+ } else if (start2) {
3359
+ startMs = parseTimeInput(start2);
3360
+ if (startMs === null) {
3361
+ throw new UserActionableError(
3362
+ `Invalid --start value "${start2}". Provide an ISO-8601 timestamp or a Unix epoch (seconds).`
3363
+ );
3364
+ }
3365
+ } else {
3366
+ throw new UserActionableError(
3367
+ "Provide a time window with --since <duration> or --start <time>."
3368
+ );
3369
+ }
3370
+ if (startMs > endMs) {
3371
+ throw new UserActionableError("Invalid time window: --start must be before or equal to --end.");
3372
+ }
3373
+ if (endMs - startMs > SEVEN_DAYS_MS) {
3374
+ throw new UserActionableError("Invalid time window: the range must not exceed 7 days.");
3375
+ }
3376
+ return { start: start2, end };
3377
+ }
3378
+ var LEVEL_COLORS = {
3379
+ debug: "gray",
3380
+ info: "green",
3381
+ warn: "yellow",
3382
+ error: "red"
3383
+ };
3384
+ var UNKNOWN_TIME = "unknown";
3385
+ function isRedundantExceptionName(name, message) {
3386
+ const parts = name.split("<*>").map((part) => part.trim()).filter((part) => part.length > 0);
3387
+ return parts.length > 0 && parts.every((part) => message.includes(part));
3388
+ }
3389
+ var stringifyMessage = (message) => typeof message === "string" ? message : JSON.stringify(message);
3390
+ function formatLogEntry(entry, format = "default") {
3391
+ const message = (entry.messages ?? []).map(stringifyMessage).join(" ").replace(/\s+/g, " ").trim();
3392
+ if (format === "short") return message;
3393
+ const level = entry.level ?? "unknown";
3394
+ const coloredLevel = colorize8(LEVEL_COLORS[level.toLowerCase()] ?? "white", level.toUpperCase());
3395
+ const exceptionStr = entry.is_exception && entry.exception_name && !isRedundantExceptionName(entry.exception_name, message) ? ` ${colorize8("red", `[${entry.exception_name}]`)}` : "";
3396
+ let requestStr = "";
3397
+ if (format === "request" && entry.request) {
3398
+ const parts = [];
3399
+ if (entry.request.method) parts.push(entry.request.method);
3400
+ if (entry.request.url) parts.push(entry.request.url);
3401
+ const status = entry.request.status_code ? ` (${entry.request.status_code})` : "";
3402
+ if (parts.length > 0) requestStr = ` ${parts.join(" ")}${status}`;
3403
+ }
3404
+ return `[${entry.timestamp ?? UNKNOWN_TIME}] [${coloredLevel}]${requestStr}${exceptionStr} ${message}`;
3405
+ }
3406
+ async function queryLogs(projectUuid, storeHash, accessToken, apiHost, params) {
3407
+ const search = new URLSearchParams();
3408
+ search.set("start", params.start);
3409
+ search.set("end", params.end);
3410
+ if (params.method) search.set("method", params.method);
3411
+ if (params.statusCode != null) search.set("status_code", String(params.statusCode));
3412
+ if (params.urlLike) search.set("url:like", params.urlLike);
3413
+ if (params.levelMin) search.set("level:min", params.levelMin);
3414
+ if (params.limit != null) search.set("limit", String(params.limit));
3415
+ const response = await fetch(
3416
+ `https://${apiHost}/stores/${storeHash}/v3/infrastructure/logs/${projectUuid}?${search.toString()}`,
3417
+ {
3418
+ method: "GET",
3419
+ headers: {
3420
+ "X-Auth-Token": accessToken,
3421
+ "X-Correlation-Id": getTelemetry().correlationId,
3422
+ Accept: "application/json"
3423
+ }
3424
+ }
3425
+ );
3426
+ assertAuthorized(response);
3427
+ if (response.status === 403) {
3428
+ throw new UserActionableError(
3429
+ "Infrastructure Logs API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it."
3430
+ );
3431
+ }
3432
+ if (response.status === 404) {
3433
+ throw new UserActionableError("Project not found. Check the project UUID.");
3434
+ }
3435
+ if (response.status === 400 || response.status === 422) {
3436
+ const body = await response.json().catch(() => null);
3437
+ throw new UserActionableError(formatV3Error(body) ?? "Invalid log query.");
3438
+ }
3439
+ if (!response.ok) {
3440
+ throw await httpError(response, "Failed to fetch logs");
3441
+ }
3442
+ const res = await response.json();
3443
+ return queryLogsSchema.parse(res);
3444
+ }
3445
+
3446
+ // src/cli/lib/domains.ts
3447
+ var domainStatusSchema = z15.enum(["pending", "verified", "failed", "unknown"]);
3448
+ var domainSchema = z15.object({
3449
+ domain: z15.string(),
3450
+ project_uuid: z15.string(),
3451
+ verification_status: domainStatusSchema
3452
+ });
3453
+ var ownershipVerificationSchema = z15.object({
3454
+ type: z15.string(),
3455
+ name: z15.string(),
3456
+ value: z15.string().optional()
3457
+ });
3458
+ var ownershipVerificationMetaSchema = z15.object({
3459
+ meta: z15.object({
3460
+ ownership_verification: ownershipVerificationSchema
3461
+ })
3462
+ });
3463
+ var DomainOwnershipVerificationError = class extends UserActionableError {
3464
+ constructor(message, ownershipVerification) {
3465
+ super(message);
3466
+ this.name = "DomainOwnershipVerificationError";
3467
+ this.ownershipVerification = ownershipVerification;
3468
+ }
3469
+ };
3470
+ function parseOwnershipVerification(body) {
3471
+ const parsed = ownershipVerificationMetaSchema.safeParse(body);
3472
+ return parsed.success ? parsed.data.meta.ownership_verification : void 0;
3473
+ }
3474
+ var boundProjectMetaSchema = z15.object({
3475
+ meta: z15.object({
3476
+ project_uuid: z15.string()
3477
+ })
3478
+ });
3479
+ var DomainBoundToProjectError = class extends UserActionableError {
3480
+ constructor(message, projectUuid) {
3481
+ super(message);
3482
+ this.name = "DomainBoundToProjectError";
3483
+ this.projectUuid = projectUuid;
3484
+ }
3485
+ };
3486
+ function parseBoundProjectUuid(body) {
3487
+ const parsed = boundProjectMetaSchema.safeParse(body);
3488
+ return parsed.success ? parsed.data.meta.project_uuid : void 0;
3489
+ }
3490
+ var domainResponseSchema = z15.object({
3491
+ data: domainSchema
3492
+ });
3493
+ var domainListResponseSchema = z15.object({
3494
+ data: z15.array(domainSchema)
3495
+ });
3496
+ var DOMAINS_API_NOT_ENABLED = "Infrastructure Domains API not enabled. If you are part of the beta, contact support@bigcommerce.com to enable it.";
3497
+ function domainsUrl(storeHash, projectUuid, apiHost) {
3498
+ return `https://${apiHost}/stores/${storeHash}/v3/infrastructure/projects/${projectUuid}/domains`;
3499
+ }
3500
+ function domainUrl(storeHash, projectUuid, domain, apiHost) {
3501
+ return `${domainsUrl(storeHash, projectUuid, apiHost)}/${encodeURIComponent(domain)}`;
3502
+ }
3503
+ function domainClaimUrl(storeHash, projectUuid, domain, apiHost) {
3504
+ return `${domainUrl(storeHash, projectUuid, domain, apiHost)}/claim`;
3505
+ }
3506
+ function domainTransferUrl(storeHash, projectUuid, domain, apiHost) {
3507
+ return `${domainUrl(storeHash, projectUuid, domain, apiHost)}/transfer`;
3508
+ }
3509
+ function authHeaders3(accessToken) {
3510
+ return {
3511
+ Accept: "application/json",
3512
+ "X-Auth-Token": accessToken,
3513
+ "X-Correlation-Id": getTelemetry().correlationId
3514
+ };
3515
+ }
3516
+ function formatResponseStatus(response, message) {
3517
+ return [response.status, response.statusText || message].filter(Boolean).join(" ");
3518
+ }
3519
+ function buildErrorMessage(response, body, action) {
3520
+ const message = formatV3Error(body);
3521
+ if (response.status >= 500) {
3522
+ const status = formatResponseStatus(response, message);
3523
+ return `${action}: ${status}. This is a server-side response from the Domains API.`;
3524
+ }
3525
+ return message ?? `${action}: ${response.statusText}`;
3526
+ }
3527
+ async function assertDomainResponse(response, action) {
3528
+ assertAuthorized(response);
3529
+ if (response.status === 403) {
3530
+ throw new UserActionableError(DOMAINS_API_NOT_ENABLED);
3531
+ }
3532
+ if (response.ok) {
3533
+ return;
3534
+ }
3535
+ const body = await response.json().catch(() => null);
3536
+ const message = buildErrorMessage(response, body, action);
3537
+ const ownershipVerification = parseOwnershipVerification(body);
3538
+ if (ownershipVerification) {
3539
+ throw new DomainOwnershipVerificationError(message, ownershipVerification);
3540
+ }
3541
+ const boundProjectUuid = parseBoundProjectUuid(body);
3542
+ if (boundProjectUuid) {
3543
+ throw new DomainBoundToProjectError(message, boundProjectUuid);
3544
+ }
3545
+ if (response.status >= 500) {
3546
+ throw new Error(message);
3547
+ }
3548
+ throw new UserActionableError(message);
3549
+ }
3550
+ async function createDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
3551
+ const response = await fetch(domainsUrl(storeHash, projectUuid, apiHost), {
3552
+ method: "POST",
3553
+ headers: {
3554
+ ...authHeaders3(accessToken),
3555
+ "Content-Type": "application/json"
3556
+ },
3557
+ body: JSON.stringify({ domain })
3558
+ });
3559
+ await assertDomainResponse(response, "Failed to add domain");
3560
+ const result = await response.json();
3561
+ return domainResponseSchema.parse(result).data;
3562
+ }
3563
+ async function getDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
3564
+ const response = await fetch(domainUrl(storeHash, projectUuid, domain, apiHost), {
3565
+ method: "GET",
3566
+ headers: authHeaders3(accessToken)
3567
+ });
3568
+ await assertDomainResponse(response, "Failed to fetch domain");
3569
+ const result = await response.json();
3570
+ return domainResponseSchema.parse(result).data;
3571
+ }
3572
+ async function findDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
3573
+ const response = await fetch(domainUrl(storeHash, projectUuid, domain, apiHost), {
3574
+ method: "GET",
3575
+ headers: authHeaders3(accessToken)
3576
+ });
3577
+ if (response.status === 404) {
3578
+ return null;
3579
+ }
3580
+ await assertDomainResponse(response, "Failed to fetch domain");
3581
+ const result = await response.json();
3582
+ return domainResponseSchema.parse(result).data;
3583
+ }
3584
+ async function listDomains(projectUuid, storeHash, accessToken, apiHost, filters = {}) {
3585
+ const search = new URLSearchParams();
3586
+ if (filters.domains && filters.domains.length > 0) {
3587
+ search.set("domain:in", filters.domains.join(","));
3588
+ }
3589
+ if (filters.verificationStatus) {
3590
+ search.set("verification_status", filters.verificationStatus);
3591
+ }
3592
+ const query2 = search.toString();
3593
+ const url = `${domainsUrl(storeHash, projectUuid, apiHost)}${query2 ? `?${query2}` : ""}`;
3594
+ const response = await fetch(url, {
3595
+ method: "GET",
3596
+ headers: authHeaders3(accessToken)
3597
+ });
3598
+ await assertDomainResponse(response, "Failed to fetch domains");
3599
+ const result = await response.json();
3600
+ return domainListResponseSchema.parse(result).data;
3601
+ }
3602
+ async function deleteDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
3603
+ const response = await fetch(domainUrl(storeHash, projectUuid, domain, apiHost), {
3604
+ method: "DELETE",
3605
+ headers: authHeaders3(accessToken)
3606
+ });
3607
+ await assertDomainResponse(response, `Failed to remove domain: ${response.statusText}`);
3608
+ }
3609
+ async function claimDomain(domain, projectUuid, storeHash, accessToken, apiHost) {
3610
+ const response = await fetch(domainClaimUrl(storeHash, projectUuid, domain, apiHost), {
3611
+ method: "POST",
3612
+ headers: authHeaders3(accessToken)
3613
+ });
3614
+ await assertDomainResponse(response, "Failed to claim domain");
3615
+ }
3616
+ async function transferDomain(domain, projectUuid, newProjectUuid, storeHash, accessToken, apiHost) {
3617
+ const response = await fetch(domainTransferUrl(storeHash, projectUuid, domain, apiHost), {
3618
+ method: "POST",
3619
+ headers: {
3620
+ ...authHeaders3(accessToken),
3621
+ "Content-Type": "application/json"
3622
+ },
3623
+ body: JSON.stringify({ new_project_uuid: newProjectUuid })
3624
+ });
3625
+ await assertDomainResponse(response, "Failed to transfer domain");
3626
+ }
3627
+
3628
+ // src/cli/commands/domains.ts
3629
+ var WAIT_INTERVAL_MS = 5e3;
3630
+ var WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
3631
+ var DOMAIN_STATUS_FILTERS = ["pending", "verified", "failed"];
3632
+ var STATUS_COLORS = {
3633
+ pending: "yellow",
3634
+ verified: "green",
3635
+ failed: "red",
3636
+ unknown: "gray"
3637
+ };
3638
+ var STATUS_LABELS = {
3639
+ pending: "pending",
3640
+ verified: "active",
3641
+ failed: "failed",
3642
+ unknown: "unknown"
3643
+ };
3644
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
3645
+ function resolveDomainCommandContext(options) {
3646
+ const config2 = getProjectConfig();
3647
+ const apiHost = resolveApiHost(options, config2);
3648
+ const { storeHash, accessToken } = resolveCredentials(options, config2);
3649
+ const projectUuid = resolveProjectUuid(options);
3650
+ return {
3651
+ projectUuid,
3652
+ storeHash,
3653
+ accessToken,
3654
+ apiHost
3655
+ };
3656
+ }
3657
+ async function resolveDestinationProject(domain, context, toProjectUuid) {
3658
+ if (toProjectUuid) {
3659
+ if (toProjectUuid === context.projectUuid) {
3660
+ throw new UserActionableError("The destination project must differ from the source project.");
3661
+ }
3662
+ return toProjectUuid;
3663
+ }
3664
+ consola.start("Fetching projects...");
3665
+ const projects = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
3666
+ consola.success("Projects fetched.");
3667
+ const destinations = projects.filter((project2) => project2.uuid !== context.projectUuid);
3668
+ if (destinations.length === 0) {
3669
+ throw new UserActionableError(
3670
+ "No other projects to transfer to. Create another project with `catalyst project create` first."
3671
+ );
3672
+ }
3673
+ return select6({
3674
+ message: `Select the project to transfer ${domain} to (Press <enter> to select).`,
3675
+ choices: destinations.map((project2) => ({
3676
+ name: project2.name,
3677
+ value: project2.uuid,
3678
+ description: project2.uuid
3679
+ }))
3680
+ });
3681
+ }
3682
+ function formatDomainStatus(status) {
3683
+ return colorize9(STATUS_COLORS[status], STATUS_LABELS[status]);
3684
+ }
3685
+ function formatDomain(domain) {
3686
+ return `${domain.domain} ${formatDomainStatus(domain.verification_status)}`;
3687
+ }
3688
+ function formatOwnershipVerification(record) {
3689
+ const lines = [
3690
+ "Add this DNS record to verify ownership:",
3691
+ ` Type: ${record.type}`,
3692
+ ` Name: ${record.name}`
3693
+ ];
3694
+ if (record.value) {
3695
+ lines.push(` Value: ${record.value}`);
3696
+ }
3697
+ return lines.join("\n");
3698
+ }
3699
+ async function waitForDomainVerification({
3700
+ domain,
3701
+ projectUuid,
3702
+ storeHash,
3703
+ accessToken,
3704
+ apiHost,
3705
+ intervalMs = WAIT_INTERVAL_MS,
3706
+ timeoutMs = WAIT_TIMEOUT_MS
3707
+ }) {
3708
+ const startedAt = Date.now();
3709
+ let current = await getDomain(domain, projectUuid, storeHash, accessToken, apiHost);
3710
+ while (current.verification_status === "pending" && Date.now() - startedAt < timeoutMs) {
3711
+ await sleep(intervalMs);
3712
+ current = await getDomain(domain, projectUuid, storeHash, accessToken, apiHost);
3713
+ }
3714
+ return current;
3715
+ }
3716
+ var add = new Command7("add").configureHelp({ showGlobalOptions: true }).description("Add a custom domain to the current Native Hosting project.").argument("<domain>", "Custom domain to add.").addHelpText(
3717
+ "after",
3718
+ `
3719
+ Examples:
3720
+ $ catalyst domains add www.example.com
3721
+
3722
+ # Wait until the domain leaves pending verification
3723
+ $ catalyst domains add www.example.com --wait`
3724
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--wait", "Poll until domain verification completes or times out.").action(async (domain, options) => {
3725
+ const context = resolveDomainCommandContext(options);
3726
+ await getTelemetry().identify(context.storeHash);
3727
+ consola.start(`Adding domain ${domain}...`);
3728
+ let result;
3729
+ try {
3730
+ result = await createDomain(
3731
+ domain,
3732
+ context.projectUuid,
3733
+ context.storeHash,
3734
+ context.accessToken,
3735
+ context.apiHost
3736
+ );
3737
+ } catch (error) {
3738
+ if (error instanceof DomainOwnershipVerificationError) {
3739
+ consola.warn(`${domain} is already in use on another store.`);
3740
+ consola.log(formatOwnershipVerification(error.ownershipVerification));
3741
+ consola.info("Once the record is live, claim it with:");
3742
+ consola.log(` catalyst domains claim ${domain}`);
3743
+ process.exit(1);
3744
+ return;
3745
+ }
3746
+ if (error instanceof DomainBoundToProjectError) {
3747
+ consola.warn(`${domain} is already bound to another project in this store.`);
3748
+ consola.info("To move it to this project, run:");
3749
+ consola.log(
3750
+ ` catalyst domains transfer ${domain} --project-uuid ${error.projectUuid} --to-project-uuid ${context.projectUuid}`
3751
+ );
3752
+ process.exit(1);
3753
+ return;
3754
+ }
3755
+ throw error;
3756
+ }
3757
+ consola.success(`Domain ${result.domain} added.`);
3758
+ if (options.wait && result.verification_status === "pending") {
3759
+ consola.start(`Waiting for ${result.domain} to verify...`);
3760
+ result = await waitForDomainVerification({ domain: result.domain, ...context });
3761
+ }
3762
+ consola.log(formatDomain(result));
3763
+ process.exit(0);
3764
+ });
3765
+ var list = new Command7("list").configureHelp({ showGlobalOptions: true }).description("List custom domains for the current Native Hosting project.").addHelpText(
3766
+ "after",
3767
+ `
3768
+ Examples:
3769
+ $ catalyst domains list
3770
+
3771
+ # Show pending domains only
3772
+ $ catalyst domains list --status pending`
3773
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--domain <domain>", "Only show a specific domain.").addOption(
3774
+ new Option7("--status <status>", "Filter by verification status.").choices(
3775
+ DOMAIN_STATUS_FILTERS
3776
+ )
3777
+ ).action(async (options) => {
3778
+ const context = resolveDomainCommandContext(options);
3779
+ await getTelemetry().identify(context.storeHash);
3780
+ consola.start("Fetching domains...");
3781
+ const result = await listDomains(
3782
+ context.projectUuid,
3783
+ context.storeHash,
3784
+ context.accessToken,
3785
+ context.apiHost,
3786
+ {
3787
+ domains: options.domain ? [options.domain] : void 0,
3788
+ verificationStatus: options.status
3789
+ }
3790
+ );
3791
+ consola.success("Domains fetched.");
3792
+ if (result.length === 0) {
3793
+ consola.info("No custom domains found.");
3794
+ process.exit(0);
3795
+ return;
3796
+ }
3797
+ result.forEach((item) => consola.log(formatDomain(item)));
3798
+ process.exit(0);
3799
+ });
3800
+ var showStatus = new Command7("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(
3801
+ "after",
3802
+ `
3803
+ Examples:
3804
+ $ catalyst domains status www.example.com
3805
+
3806
+ # Wait until the domain leaves pending verification
3807
+ $ catalyst domains status www.example.com --wait`
3808
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--wait", "Poll until domain verification completes or times out.").action(async (domain, options) => {
3809
+ const context = resolveDomainCommandContext(options);
3810
+ await getTelemetry().identify(context.storeHash);
3811
+ consola.start(`Fetching status for ${domain}...`);
3812
+ let result = await getDomain(
3813
+ domain,
3814
+ context.projectUuid,
3815
+ context.storeHash,
3816
+ context.accessToken,
3817
+ context.apiHost
3818
+ );
3819
+ consola.success("Domain status fetched.");
3820
+ if (options.wait && result.verification_status === "pending") {
3821
+ consola.start(`Waiting for ${result.domain} to verify...`);
3822
+ result = await waitForDomainVerification({ domain: result.domain, ...context });
3823
+ }
3824
+ consola.log(formatDomain(result));
3825
+ process.exit(0);
3826
+ });
3827
+ var claim = new Command7("claim").configureHelp({ showGlobalOptions: true }).description("Claim a custom domain that is currently in use on another store.").argument("<domain>", "Custom domain to claim.").addHelpText(
3828
+ "after",
3829
+ `
3830
+ Claiming requires publishing the ownership-verification TXT record returned when
3831
+ you try to add a domain that is already in use on another store. Publish the
3832
+ record with your DNS provider, then run this command to complete the claim.
3833
+
3834
+ Examples:
3835
+ $ catalyst domains claim www.example.com
3836
+
3837
+ # Wait until the claimed domain leaves pending verification
3838
+ $ catalyst domains claim www.example.com --wait`
3839
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--wait", "Poll until domain verification completes or times out.").action(async (domain, options) => {
3840
+ const context = resolveDomainCommandContext(options);
3841
+ await getTelemetry().identify(context.storeHash);
3842
+ consola.start(`Claiming domain ${domain}...`);
3843
+ try {
3844
+ await claimDomain(
3845
+ domain,
3846
+ context.projectUuid,
3847
+ context.storeHash,
3848
+ context.accessToken,
3849
+ context.apiHost
3850
+ );
3851
+ } catch (error) {
3852
+ if (error instanceof DomainOwnershipVerificationError) {
3853
+ consola.warn(`Ownership of ${domain} could not be verified yet.`);
3854
+ consola.log(formatOwnershipVerification(error.ownershipVerification));
3855
+ consola.info("Once the record is live, run the claim again:");
3856
+ consola.log(` catalyst domains claim ${domain}`);
3857
+ process.exit(1);
3858
+ return;
3859
+ }
3860
+ throw error;
3861
+ }
3862
+ consola.success(`Domain ${domain} claimed.`);
3863
+ let result = await getDomain(
3864
+ domain,
3865
+ context.projectUuid,
3866
+ context.storeHash,
3867
+ context.accessToken,
3868
+ context.apiHost
3869
+ );
3870
+ if (options.wait && result.verification_status === "pending") {
3871
+ consola.start(`Waiting for ${result.domain} to verify...`);
3872
+ result = await waitForDomainVerification({ domain: result.domain, ...context });
3873
+ }
3874
+ consola.log(formatDomain(result));
3875
+ process.exit(0);
3876
+ });
3877
+ var transfer = new Command7("transfer").configureHelp({ showGlobalOptions: true }).description("Transfer a custom domain to another project in the same store.").argument("<domain>", "Custom domain to transfer.").addHelpText(
3878
+ "after",
3879
+ `
3880
+ The domain is transferred from the current project to the destination project.
3881
+ Omit --to-project-uuid to pick the destination from a list of your projects.
3882
+
3883
+ Examples:
3884
+ # Pick the destination project interactively
3885
+ $ catalyst domains transfer www.example.com
3886
+
3887
+ # Transfer to a specific project
3888
+ $ catalyst domains transfer www.example.com --to-project-uuid <uuid>`
3889
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--to-project-uuid <uuid>", "Destination project UUID. Prompts to choose one if omitted.").option("--wait", "Poll until domain verification completes or times out.").action(async (domain, options) => {
3890
+ const context = resolveDomainCommandContext(options);
3891
+ await getTelemetry().identify(context.storeHash);
3892
+ consola.start(`Checking ${domain} on the current project...`);
3893
+ const owned = await findDomain(
3894
+ domain,
3895
+ context.projectUuid,
3896
+ context.storeHash,
3897
+ context.accessToken,
3898
+ context.apiHost
2656
3899
  );
2657
- if (!options.updateSiteUrl) {
3900
+ if (!owned) {
3901
+ consola.start(`Locating the project that owns ${domain}...`);
3902
+ const projects = await fetchProjects(context.storeHash, context.accessToken, context.apiHost);
3903
+ const candidates = projects.filter((project2) => project2.uuid !== context.projectUuid);
3904
+ const matches = await Promise.all(
3905
+ candidates.map(async (project2) => ({
3906
+ project: project2,
3907
+ found: await findDomain(
3908
+ domain,
3909
+ project2.uuid,
3910
+ context.storeHash,
3911
+ context.accessToken,
3912
+ context.apiHost
3913
+ )
3914
+ }))
3915
+ );
3916
+ const owner = matches.find((match) => match.found)?.project;
3917
+ if (!owner) {
3918
+ throw new UserActionableError(
3919
+ `${domain} isn't on any project in this store. If it's in use on another store, claim it with \`catalyst domains claim ${domain}\`; otherwise double-check the domain name.`
3920
+ );
3921
+ }
3922
+ const toSuffix = options.toProjectUuid ? ` --to-project-uuid ${options.toProjectUuid}` : "";
3923
+ consola.warn(
3924
+ `${domain} is on project "${owner.name}" (${owner.uuid}), not the current project.`
3925
+ );
3926
+ consola.info("Re-run the transfer from that project:");
3927
+ consola.log(` catalyst domains transfer ${domain} --project-uuid ${owner.uuid}${toSuffix}`);
3928
+ process.exit(1);
2658
3929
  return;
2659
3930
  }
2660
- try {
2661
- await runChannelSiteUrlFlow({
2662
- storeHash,
2663
- accessToken,
2664
- apiHost: options.apiHost,
2665
- projectUuid,
2666
- preferHostname: deploymentHostname
3931
+ consola.success(`${domain} found on the current project.`);
3932
+ const newProjectUuid = await resolveDestinationProject(domain, context, options.toProjectUuid);
3933
+ consola.start(`Transferring domain ${domain}...`);
3934
+ await transferDomain(
3935
+ domain,
3936
+ context.projectUuid,
3937
+ newProjectUuid,
3938
+ context.storeHash,
3939
+ context.accessToken,
3940
+ context.apiHost
3941
+ );
3942
+ consola.success(`Domain ${domain} transferred.`);
3943
+ let result = await getDomain(
3944
+ domain,
3945
+ newProjectUuid,
3946
+ context.storeHash,
3947
+ context.accessToken,
3948
+ context.apiHost
3949
+ );
3950
+ if (options.wait && result.verification_status === "pending") {
3951
+ consola.start(`Waiting for ${result.domain} to verify...`);
3952
+ result = await waitForDomainVerification({
3953
+ domain: result.domain,
3954
+ projectUuid: newProjectUuid,
3955
+ storeHash: context.storeHash,
3956
+ accessToken: context.accessToken,
3957
+ apiHost: context.apiHost
2667
3958
  });
2668
- } catch (error) {
2669
- consola.warn(
2670
- `Failed to update channel site URL: ${error instanceof Error ? error.message : String(error)}`
2671
- );
2672
- consola.info(
2673
- "Update it manually in the control panel, or re-run `catalyst auth login` if the token is missing the store_channel_settings scope."
2674
- );
2675
3959
  }
3960
+ consola.log(formatDomain(result));
3961
+ process.exit(0);
3962
+ });
3963
+ var remove = new Command7("remove").configureHelp({ showGlobalOptions: true }).description("Remove a custom domain from the current Native Hosting project.").argument("<domain>", "Custom domain to remove.").addHelpText(
3964
+ "after",
3965
+ `
3966
+ Examples:
3967
+ $ catalyst domains remove www.example.com
3968
+
3969
+ # Skip confirmation for an active domain
3970
+ $ catalyst domains remove www.example.com --force`
3971
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--force", "Skip the confirmation prompt before removing an active domain.").action(async (domain, options) => {
3972
+ const context = resolveDomainCommandContext(options);
3973
+ await getTelemetry().identify(context.storeHash);
3974
+ consola.start(`Fetching status for ${domain}...`);
3975
+ const current = await getDomain(
3976
+ domain,
3977
+ context.projectUuid,
3978
+ context.storeHash,
3979
+ context.accessToken,
3980
+ context.apiHost
3981
+ );
3982
+ if (current.verification_status === "verified" && !options.force) {
3983
+ const confirmed = await confirm4({
3984
+ message: `Remove active domain ${current.domain}? Traffic may stop routing to this project.`,
3985
+ default: false
3986
+ });
3987
+ if (!confirmed) {
3988
+ consola.info("Aborted. No domain was removed.");
3989
+ process.exit(0);
3990
+ return;
3991
+ }
3992
+ }
3993
+ consola.start(`Removing domain ${current.domain}...`);
3994
+ await deleteDomain(
3995
+ current.domain,
3996
+ context.projectUuid,
3997
+ context.storeHash,
3998
+ context.accessToken,
3999
+ context.apiHost
4000
+ );
4001
+ consola.success(`Domain ${current.domain} removed.`);
4002
+ process.exit(0);
2676
4003
  });
4004
+ var domains = new Command7("domains").configureHelp({ showGlobalOptions: true }).description("Manage custom domains for the current Native Hosting project.").addCommand(add).addCommand(list).addCommand(showStatus).addCommand(claim).addCommand(transfer).addCommand(remove);
2677
4005
 
2678
4006
  // src/cli/commands/env.ts
2679
- import { Command as Command6 } from "commander";
4007
+ import { Command as Command8 } from "commander";
2680
4008
  var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
2681
- var add = new Command6("add").configureHelp({ showGlobalOptions: true }).description(
4009
+ var add2 = new Command8("add").configureHelp({ showGlobalOptions: true }).description(
2682
4010
  "Add or update one or more deployment environment variables. Stored in .bigcommerce/project.json and sent as secrets on every `catalyst deploy`."
2683
4011
  ).argument("<vars...>", "One or more environment variables in KEY=VALUE format.").addHelpText(
2684
4012
  "after",
@@ -2700,7 +4028,7 @@ Example:
2700
4028
  keys.forEach((key) => consola.log(` ${key}=${MASK}`));
2701
4029
  process.exit(0);
2702
4030
  });
2703
- 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(
4031
+ var remove2 = new Command8("remove").configureHelp({ showGlobalOptions: true }).description("Remove one or more stored deployment environment variables.").argument("<keys...>", "One or more environment variable names to remove.").addHelpText(
2704
4032
  "after",
2705
4033
  `
2706
4034
  Example:
@@ -2726,7 +4054,7 @@ Example:
2726
4054
  }
2727
4055
  process.exit(0);
2728
4056
  });
2729
- var list = new Command6("list").configureHelp({ showGlobalOptions: true }).description("List stored deployment environment variables (values are masked).").addHelpText(
4057
+ var list2 = new Command8("list").configureHelp({ showGlobalOptions: true }).description("List stored deployment environment variables (values are masked).").addHelpText(
2730
4058
  "after",
2731
4059
  `
2732
4060
  Example:
@@ -2743,41 +4071,41 @@ Example:
2743
4071
  keys.forEach((key) => consola.log(`${key}=${MASK}`));
2744
4072
  process.exit(0);
2745
4073
  });
2746
- var env = new Command6("env").configureHelp({ showGlobalOptions: true }).description(
4074
+ var env = new Command8("env").configureHelp({ showGlobalOptions: true }).description(
2747
4075
  "Manage persistent deployment environment variables. These are sent as secrets on every `catalyst deploy`, so you no longer need to pass `--secret` each time."
2748
- ).addCommand(add).addCommand(remove).addCommand(list);
4076
+ ).addCommand(add2).addCommand(remove2).addCommand(list2);
2749
4077
 
2750
4078
  // src/cli/commands/logs.ts
2751
- import { Command as Command7, Option as Option7 } from "commander";
2752
- import { colorize as colorize6 } from "consola/utils";
2753
- import { z as z11 } from "zod";
4079
+ import { Command as Command9, InvalidArgumentError as InvalidArgumentError4, Option as Option8 } from "commander";
4080
+ import { colorize as colorize10 } from "consola/utils";
4081
+ import { z as z16 } from "zod";
2754
4082
  var telemetry = new Telemetry();
2755
4083
  var DEFAULT_CONNECTION_TTL_MS = 1 * 60 * 1e3;
2756
4084
  var MAX_RETRIES = 5;
2757
4085
  var isFatalStatusCode = (status) => status >= 400 && status < 500;
2758
- var LEVEL_COLORS = {
4086
+ var LEVEL_COLORS2 = {
2759
4087
  INFO: "green",
2760
4088
  WARN: "yellow",
2761
4089
  ERROR: "red",
2762
4090
  DEBUG: "gray"
2763
4091
  };
2764
- var LogEventSchema = z11.object({
2765
- uuid: z11.string(),
2766
- project_uuid: z11.string(),
2767
- request: z11.object({
2768
- method: z11.string(),
2769
- url: z11.string(),
2770
- status_code: z11.number()
4092
+ var LogEventSchema = z16.object({
4093
+ uuid: z16.string(),
4094
+ project_uuid: z16.string(),
4095
+ request: z16.object({
4096
+ method: z16.string(),
4097
+ url: z16.string(),
4098
+ status_code: z16.number()
2771
4099
  }),
2772
- logs: z11.array(
2773
- z11.object({
2774
- timestamp: z11.string(),
2775
- level: z11.string(),
2776
- messages: z11.array(z11.unknown())
4100
+ logs: z16.array(
4101
+ z16.object({
4102
+ timestamp: z16.string(),
4103
+ level: z16.string(),
4104
+ messages: z16.array(z16.unknown())
2777
4105
  })
2778
4106
  ),
2779
- exceptions: z11.array(z11.unknown()),
2780
- timestamp: z11.string()
4107
+ exceptions: z16.array(z16.unknown()),
4108
+ timestamp: z16.string()
2781
4109
  }).loose();
2782
4110
  var StreamError = class extends Error {
2783
4111
  constructor(message, fatal) {
@@ -2791,7 +4119,7 @@ var formatLogEvent = (event, format) => {
2791
4119
  logEntries.forEach((entry) => {
2792
4120
  const msg = formatMessages(entry.messages);
2793
4121
  const level = entry.level.toUpperCase();
2794
- const coloredLevel = colorize6(LEVEL_COLORS[level] ?? "white", level);
4122
+ const coloredLevel = colorize10(LEVEL_COLORS2[level] ?? "white", level);
2795
4123
  switch (format) {
2796
4124
  case "short":
2797
4125
  consola.log(msg);
@@ -2862,10 +4190,8 @@ var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
2862
4190
  throw new StreamError(new UnauthorizedError().message, true);
2863
4191
  }
2864
4192
  if (!response.ok) {
2865
- throw new StreamError(
2866
- `Failed to open log stream: ${response.status} ${response.statusText}`,
2867
- isFatalStatusCode(response.status)
2868
- );
4193
+ const error = await httpError(response, "Failed to open log stream");
4194
+ throw new StreamError(error.message, isFatalStatusCode(response.status));
2869
4195
  }
2870
4196
  const reader = response.body?.getReader();
2871
4197
  if (!reader) {
@@ -2934,7 +4260,7 @@ var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, conn
2934
4260
  }
2935
4261
  }
2936
4262
  };
2937
- var tail = new Command7("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
4263
+ var tail = new Command9("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
2938
4264
  "after",
2939
4265
  `
2940
4266
  Examples:
@@ -2946,44 +4272,127 @@ Examples:
2946
4272
  # Tail logs as raw JSON (useful for piping to other tools)
2947
4273
  $ catalyst logs tail --format json`
2948
4274
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
2949
- new Option7("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
4275
+ new Option8("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
2950
4276
  ).action(async (options) => {
2951
4277
  try {
2952
4278
  const config2 = getProjectConfig();
4279
+ const apiHost = resolveApiHost(options, config2);
2953
4280
  const { storeHash, accessToken } = resolveCredentials(options, config2);
2954
4281
  await telemetry.identify(storeHash);
2955
4282
  const projectUuid = resolveProjectUuid(options);
2956
- await tailLogs(projectUuid, storeHash, accessToken, options.apiHost, options.format);
4283
+ await tailLogs(projectUuid, storeHash, accessToken, apiHost, options.format);
2957
4284
  } catch (error) {
2958
4285
  consola.error(error);
2959
4286
  process.exit(1);
2960
4287
  }
2961
4288
  });
2962
- var query = new Command7("query").configureHelp({ showGlobalOptions: true }).description("Query historical logs from your deployed application.").addHelpText(
4289
+ var parseIntInRange = (flag, min, max) => (value) => {
4290
+ const parsed = Number(value);
4291
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
4292
+ throw new InvalidArgumentError4(`${flag} must be an integer between ${min} and ${max}.`);
4293
+ }
4294
+ return parsed;
4295
+ };
4296
+ var query = new Command9("query").configureHelp({ showGlobalOptions: true }).description("Query historical logs from your deployed application.").addHelpText(
2963
4297
  "after",
2964
4298
  `
2965
- Example:
2966
- $ catalyst logs query`
2967
- ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).action((options) => {
2968
- const config2 = getProjectConfig();
2969
- resolveCredentials(options, config2);
2970
- consola.error("The query command is not yet implemented.");
2971
- process.exit(1);
4299
+ Specify a time window with \`--since\` (relative to now) or \`--start\`/\`--end\`
4300
+ (ISO-8601 timestamps or Unix epoch seconds, UTC). The window may not exceed
4301
+ 7 days. Entries print oldest-first; timestamps are UTC.
4302
+
4303
+ Examples:
4304
+ # Last hour of logs
4305
+ $ catalyst logs query --since 1h
4306
+
4307
+ # Everything from today (UTC)
4308
+ $ catalyst logs query --start 2026-06-11T00:00:00Z
4309
+
4310
+ # Errors only for a specific path
4311
+ $ catalyst logs query --since 24h --level-min error --url-like /cart
4312
+
4313
+ # Errors with request details (method, URL, status)
4314
+ $ catalyst logs query --since 1h --level-min error --format request
4315
+
4316
+ # Raw JSON (NDJSON) for piping to other tools
4317
+ $ catalyst logs query --since 2d --format json`
4318
+ ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
4319
+ new Option8(
4320
+ "--since <duration>",
4321
+ "Relative window ending at --end (default: now), e.g. 30m, 6h, 2d (units: s, m, h, d)."
4322
+ ).conflicts("start")
4323
+ ).option("--start <time>", "Window start: ISO-8601 timestamp or Unix epoch (seconds).").option(
4324
+ "--end <time>",
4325
+ "Window end: ISO-8601 timestamp or Unix epoch (seconds). Defaults to now. The window must not exceed 7 days."
4326
+ ).option("--method <method>", "Filter by request HTTP method (case-insensitive).").addOption(
4327
+ new Option8("--status-code <code>", "Filter by response status code (100-599).").argParser(
4328
+ parseIntInRange("--status-code", 100, 599)
4329
+ )
4330
+ ).option("--url-like <substring>", "Filter by URL substring (case-sensitive).").addOption(
4331
+ new Option8("--level-min <level>", "Minimum log level.").choices(Array.from(LOG_LEVELS))
4332
+ ).addOption(
4333
+ new Option8("--limit <n>", "Maximum number of entries to return (1-500).").argParser(parseIntInRange("--limit", 1, 500)).default(100)
4334
+ ).addOption(
4335
+ new Option8("--format <format>", "Output format for log entries.").choices(["json", "pretty", "default", "short", "request"]).default("default")
4336
+ ).action(async (options) => {
4337
+ try {
4338
+ const config2 = getProjectConfig();
4339
+ const apiHost = resolveApiHost(options, config2);
4340
+ const { storeHash, accessToken } = resolveCredentials(options, config2);
4341
+ await telemetry.identify(storeHash);
4342
+ const projectUuid = resolveProjectUuid(options);
4343
+ const { start: start2, end } = resolveTimeWindow(options);
4344
+ const result = await queryLogs(projectUuid, storeHash, accessToken, apiHost, {
4345
+ start: start2,
4346
+ end,
4347
+ method: options.method,
4348
+ statusCode: options.statusCode,
4349
+ urlLike: options.urlLike,
4350
+ levelMin: options.levelMin,
4351
+ limit: options.limit
4352
+ });
4353
+ const entries = result.data;
4354
+ const ordered = [...entries].reverse();
4355
+ if (options.format === "json") {
4356
+ ordered.forEach((entry) => process.stdout.write(`${JSON.stringify(entry)}
4357
+ `));
4358
+ return;
4359
+ }
4360
+ if (options.format === "pretty") {
4361
+ ordered.forEach((entry) => consola.log(JSON.stringify(entry, null, 2)));
4362
+ return;
4363
+ }
4364
+ if (entries.length === 0) {
4365
+ consola.info("No log entries found for the given window and filters.");
4366
+ return;
4367
+ }
4368
+ const lineFormat = options.format;
4369
+ ordered.forEach((entry) => consola.log(formatLogEntry(entry, lineFormat)));
4370
+ consola.info(
4371
+ `${entries.length} ${entries.length === 1 ? "entry" : "entries"} shown (oldest first, times in UTC).`
4372
+ );
4373
+ if (entries.length === options.limit) {
4374
+ consola.info(
4375
+ `Limit of ${options.limit} reached; older entries may exist. Narrow the time window or raise --limit (max 500) to see more.`
4376
+ );
4377
+ }
4378
+ } catch (error) {
4379
+ consola.error(error instanceof Error ? error.message : error);
4380
+ process.exit(1);
4381
+ }
2972
4382
  });
2973
- var logs = new Command7("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
4383
+ var logs = new Command9("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
2974
4384
 
2975
4385
  // src/cli/commands/project.ts
2976
- import { confirm as confirm4, input as input4, select as select5 } from "@inquirer/prompts";
2977
- import { Command as Command8 } from "commander";
2978
- import { colorize as colorize7 } from "consola/utils";
2979
- import { dirname as dirname5 } from "path";
4386
+ import { confirm as confirm5, input as input5, select as select7 } from "@inquirer/prompts";
4387
+ import { Command as Command10 } from "commander";
4388
+ import { colorize as colorize11 } from "consola/utils";
2980
4389
  async function offerCommerceHostingSetup(projectUuid, credentials) {
2981
- const projectDir = dirname5(process.cwd());
4390
+ const projectDir = process.cwd();
2982
4391
  if (getProjectState().isTransformed) {
2983
4392
  await cleanupCloudflareIncompatibilities(projectDir);
2984
4393
  return;
2985
4394
  }
2986
- const shouldSetup = await confirm4({
4395
+ const shouldSetup = await confirm5({
2987
4396
  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?",
2988
4397
  default: true
2989
4398
  });
@@ -2995,19 +4404,21 @@ async function offerCommerceHostingSetup(projectUuid, credentials) {
2995
4404
  accessToken: credentials?.accessToken
2996
4405
  });
2997
4406
  consola.success("Commerce Hosting setup complete.");
2998
- await installDependencies(projectDir);
4407
+ const packageManager = await detectProjectPackageManager(projectDir);
4408
+ await installDependencies(projectDir, packageManager);
2999
4409
  }
3000
- var list2 = new Command8("list").configureHelp({ showGlobalOptions: true }).description("List BigCommerce infrastructure projects for your store.").addHelpText(
4410
+ var list3 = new Command10("list").configureHelp({ showGlobalOptions: true }).description("List BigCommerce infrastructure projects for your store.").addHelpText(
3001
4411
  "after",
3002
4412
  `
3003
4413
  Example:
3004
4414
  $ catalyst project list`
3005
4415
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).action(async (options) => {
3006
4416
  const config2 = getProjectConfig();
4417
+ const apiHost = resolveApiHost(options, config2);
3007
4418
  const { storeHash, accessToken } = resolveCredentials(options, config2);
3008
4419
  await getTelemetry().identify(storeHash);
3009
4420
  consola.start("Fetching projects...");
3010
- const projects = await fetchProjects(storeHash, accessToken, options.apiHost);
4421
+ const projects = await fetchProjects(storeHash, accessToken, apiHost);
3011
4422
  consola.success("Projects fetched.");
3012
4423
  if (projects.length === 0) {
3013
4424
  consola.info("No projects found.");
@@ -3016,16 +4427,16 @@ Example:
3016
4427
  }
3017
4428
  const linkedProjectUuid = config2.get("projectUuid");
3018
4429
  const projectList = projects.map((p) => {
3019
- const marker = p.uuid === linkedProjectUuid ? ` ${colorize7("green", "[linked]")}` : "";
4430
+ const marker = p.uuid === linkedProjectUuid ? ` ${colorize11("green", "[linked]")}` : "";
3020
4431
  const hostnames = p.deployment_hostnames.length === 0 ? [" (not deployed)"] : p.deployment_hostnames.map(
3021
- (hostname) => ` ${colorize7("blue", `https://${hostname}`)}`
4432
+ (hostname) => ` ${colorize11("blue", `https://${hostname}`)}`
3022
4433
  );
3023
4434
  return [`${p.name} (${p.uuid})${marker}`, ...hostnames].join("\n");
3024
4435
  });
3025
4436
  consola.log(projectList.join("\n\n"));
3026
4437
  process.exit(0);
3027
4438
  });
3028
- var create2 = new Command8("create").configureHelp({ showGlobalOptions: true }).description(
4439
+ var create3 = new Command10("create").configureHelp({ showGlobalOptions: true }).description(
3029
4440
  "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."
3030
4441
  ).addHelpText(
3031
4442
  "after",
@@ -3034,6 +4445,7 @@ Example:
3034
4445
  $ catalyst project create`
3035
4446
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).action(async (options) => {
3036
4447
  const config2 = getProjectConfig();
4448
+ const apiHost = resolveApiHost(options, config2);
3037
4449
  let storeHash = options.storeHash ?? config2.get("storeHash");
3038
4450
  let accessToken = options.accessToken ?? config2.get("accessToken");
3039
4451
  if (!storeHash || !accessToken) {
@@ -3041,7 +4453,7 @@ Example:
3041
4453
  "You're not logged in yet. Let's get you authenticated before creating a project."
3042
4454
  );
3043
4455
  try {
3044
- const credentials = await login(options.loginUrl, options.apiHost);
4456
+ const credentials = await login(options.loginUrl, apiHost);
3045
4457
  storeHash = credentials.storeHash;
3046
4458
  accessToken = credentials.accessToken;
3047
4459
  config2.set("storeHash", storeHash);
@@ -3059,8 +4471,8 @@ Example:
3059
4471
  }
3060
4472
  }
3061
4473
  await getTelemetry().identify(storeHash);
3062
- const newProjectName = await input4({ message: "Enter a name for the new project:" });
3063
- const data = await createProject(newProjectName, storeHash, accessToken, options.apiHost);
4474
+ const newProjectName = await input5({ message: "Enter a name for the new project:" });
4475
+ const data = await createProject(newProjectName, storeHash, accessToken, apiHost);
3064
4476
  consola.success(`Project "${data.name}" created successfully.`);
3065
4477
  consola.start("Writing project UUID to .bigcommerce/project.json...");
3066
4478
  config2.set("projectUuid", data.uuid);
@@ -3069,7 +4481,7 @@ Example:
3069
4481
  consola.success("Project UUID written to .bigcommerce/project.json.");
3070
4482
  process.exit(0);
3071
4483
  });
3072
- var link2 = new Command8("link").configureHelp({ showGlobalOptions: true }).description(
4484
+ var link2 = new Command10("link").configureHelp({ showGlobalOptions: true }).description(
3073
4485
  "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."
3074
4486
  ).addHelpText(
3075
4487
  "after",
@@ -3082,6 +4494,7 @@ Examples:
3082
4494
  $ catalyst project link --project-uuid <UUID>`
3083
4495
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).action(async (options) => {
3084
4496
  const config2 = getProjectConfig();
4497
+ const apiHost = resolveApiHost(options, config2);
3085
4498
  const writeProjectConfig = (uuid, credentials) => {
3086
4499
  consola.start("Writing project UUID to .bigcommerce/project.json...");
3087
4500
  config2.set("projectUuid", uuid);
@@ -3102,7 +4515,7 @@ Examples:
3102
4515
  let selected;
3103
4516
  try {
3104
4517
  selected = await selectOrCreateInfrastructureProject(
3105
- { storeHash, accessToken, apiHost: options.apiHost },
4518
+ { storeHash, accessToken, apiHost },
3106
4519
  config2.get("projectUuid")
3107
4520
  );
3108
4521
  } catch (error) {
@@ -3119,7 +4532,7 @@ Examples:
3119
4532
  await offerCommerceHostingSetup(selected.uuid, { storeHash, accessToken });
3120
4533
  process.exit(0);
3121
4534
  });
3122
- var del = new Command8("delete").configureHelp({ showGlobalOptions: true }).description(
4535
+ var del = new Command10("delete").configureHelp({ showGlobalOptions: true }).description(
3123
4536
  "Permanently delete a BigCommerce infrastructure project. This action is irreversible."
3124
4537
  ).addHelpText(
3125
4538
  "after",
@@ -3135,13 +4548,14 @@ Examples:
3135
4548
  $ catalyst project delete --project-uuid <UUID> --force`
3136
4549
  ).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--force", "Skip the confirmation prompt before deleting.").action(async (options) => {
3137
4550
  const config2 = getProjectConfig();
4551
+ const apiHost = resolveApiHost(options, config2);
3138
4552
  const { storeHash, accessToken } = resolveCredentials(options, config2);
3139
4553
  await getTelemetry().identify(storeHash);
3140
4554
  let targetUuid = options.projectUuid;
3141
4555
  let targetName;
3142
4556
  if (!targetUuid) {
3143
4557
  consola.start("Fetching projects...");
3144
- const projects = await fetchProjects(storeHash, accessToken, options.apiHost);
4558
+ const projects = await fetchProjects(storeHash, accessToken, apiHost);
3145
4559
  consola.success("Projects fetched.");
3146
4560
  if (projects.length === 0) {
3147
4561
  consola.info("No projects found.");
@@ -3149,11 +4563,11 @@ Examples:
3149
4563
  return;
3150
4564
  }
3151
4565
  const linkedProjectUuid = config2.get("projectUuid");
3152
- const selected = await select5({
4566
+ const selected = await select7({
3153
4567
  message: "Select a project to delete (Press <enter> to select).",
3154
4568
  choices: [
3155
4569
  ...projects.map((p) => ({
3156
- name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize7("green", "[linked]")}` : p.name,
4570
+ name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize11("green", "[linked]")}` : p.name,
3157
4571
  value: p.uuid,
3158
4572
  description: p.uuid
3159
4573
  })),
@@ -3174,7 +4588,7 @@ Examples:
3174
4588
  }
3175
4589
  if (!options.force) {
3176
4590
  const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
3177
- const confirmed = await confirm4({
4591
+ const confirmed = await confirm5({
3178
4592
  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.`,
3179
4593
  default: false
3180
4594
  });
@@ -3185,7 +4599,7 @@ Examples:
3185
4599
  }
3186
4600
  }
3187
4601
  consola.start(`Deleting project ${targetUuid}...`);
3188
- await deleteProject(targetUuid, storeHash, accessToken, options.apiHost);
4602
+ await deleteProject(targetUuid, storeHash, accessToken, apiHost);
3189
4603
  consola.success(`Project ${targetUuid} deleted.`);
3190
4604
  if (config2.get("projectUuid") === targetUuid) {
3191
4605
  config2.delete("projectUuid");
@@ -3193,14 +4607,14 @@ Examples:
3193
4607
  }
3194
4608
  process.exit(0);
3195
4609
  });
3196
- var project = new Command8("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create2).addCommand(list2).addCommand(link2).addCommand(del);
4610
+ var project = new Command10("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create3).addCommand(list3).addCommand(link2).addCommand(del);
3197
4611
 
3198
4612
  // src/cli/commands/start.ts
3199
- import { Command as Command9 } from "commander";
3200
- import { execa as execa2 } from "execa";
3201
- import { existsSync as existsSync4, lstatSync as lstatSync2, symlinkSync as symlinkSync2 } from "fs";
3202
- import { join as join12, relative } from "path";
3203
- var start = new Command9("start").configureHelp({ showGlobalOptions: true }).description(
4613
+ import { Command as Command11 } from "commander";
4614
+ import { execa as execa3 } from "execa";
4615
+ import { existsSync as existsSync6, lstatSync, symlinkSync } from "fs";
4616
+ import { join as join13, relative } from "path";
4617
+ var start = new Command11("start").configureHelp({ showGlobalOptions: true }).description(
3204
4618
  "Start a local preview of your Catalyst storefront using the OpenNext Cloudflare adapter."
3205
4619
  ).addHelpText(
3206
4620
  "after",
@@ -3211,20 +4625,20 @@ Example:
3211
4625
  const state = getProjectState();
3212
4626
  if (!state.isTransformed) {
3213
4627
  consola.info("Project is not set up for Commerce Hosting \u2014 running `next start`.");
3214
- await execa2("pnpm", ["exec", "next", "start"], {
4628
+ await execa3("pnpm", ["exec", "next", "start"], {
3215
4629
  stdio: "inherit",
3216
4630
  cwd: process.cwd()
3217
4631
  });
3218
4632
  return;
3219
4633
  }
3220
- const envLocal = join12(process.cwd(), ".env.local");
3221
- const devVars = join12(process.cwd(), ".bigcommerce", ".dev.vars");
3222
- if (existsSync4(envLocal)) {
3223
- if (!existsSync4(devVars)) {
3224
- const target = relative(join12(process.cwd(), ".bigcommerce"), envLocal);
3225
- symlinkSync2(target, devVars);
4634
+ const envLocal = join13(process.cwd(), ".env.local");
4635
+ const devVars = join13(process.cwd(), ".bigcommerce", ".dev.vars");
4636
+ if (existsSync6(envLocal)) {
4637
+ if (!existsSync6(devVars)) {
4638
+ const target = relative(join13(process.cwd(), ".bigcommerce"), envLocal);
4639
+ symlinkSync(target, devVars);
3226
4640
  consola.success("Symlinked .bigcommerce/.dev.vars \u2192 .env.local");
3227
- } else if (!lstatSync2(devVars).isSymbolicLink()) {
4641
+ } else if (!lstatSync(devVars).isSymbolicLink()) {
3228
4642
  consola.warn(
3229
4643
  ".bigcommerce/.dev.vars already exists and is not a symlink. Wrangler will use it instead of .env.local."
3230
4644
  );
@@ -3234,14 +4648,14 @@ Example:
3234
4648
  "No .env.local file found. The preview server may fail without environment variables.\nCreate a .env.local file in your project root with the required variables."
3235
4649
  );
3236
4650
  }
3237
- await execa2(
4651
+ await execa3(
3238
4652
  "pnpm",
3239
4653
  [
3240
4654
  "exec",
3241
4655
  "opennextjs-cloudflare",
3242
4656
  "preview",
3243
4657
  "--config",
3244
- join12(".bigcommerce", "wrangler.jsonc")
4658
+ join13(".bigcommerce", "wrangler.jsonc")
3245
4659
  ],
3246
4660
  {
3247
4661
  stdio: "inherit",
@@ -3251,11 +4665,11 @@ Example:
3251
4665
  });
3252
4666
 
3253
4667
  // src/cli/commands/telemetry.ts
3254
- import { Argument, Command as Command10, Option as Option8 } from "commander";
3255
- import { colorize as colorize8 } from "consola/utils";
4668
+ import { Argument, Command as Command12, Option as Option9 } from "commander";
4669
+ import { colorize as colorize12 } from "consola/utils";
3256
4670
  var telemetryService = getTelemetry();
3257
4671
  var isEnabled = telemetryService.isEnabled();
3258
- var telemetry2 = new Command10("telemetry").configureHelp({ showGlobalOptions: true }).description(
4672
+ var telemetry2 = new Command12("telemetry").configureHelp({ showGlobalOptions: true }).description(
3259
4673
  "View or change CLI telemetry collection status. Enabling telemetry helps BigCommerce support diagnose and troubleshoot errors you encounter when using the CLI."
3260
4674
  ).addHelpText(
3261
4675
  "after",
@@ -3269,7 +4683,7 @@ Examples:
3269
4683
 
3270
4684
  # Disable telemetry collection
3271
4685
  $ catalyst telemetry --disable`
3272
- ).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) => {
4686
+ ).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) => {
3273
4687
  if (options.enable || arg === "enable") {
3274
4688
  telemetryService.setEnabled(true);
3275
4689
  isEnabled = true;
@@ -3286,7 +4700,7 @@ Examples:
3286
4700
  consola.info("Catalyst CLI Telemetry\n");
3287
4701
  }
3288
4702
  consola.info(
3289
- `Status: ${colorize8("bold", isEnabled ? colorize8("green", "Enabled") : colorize8("red", "Disabled"))}`
4703
+ `Status: ${colorize12("bold", isEnabled ? colorize12("green", "Enabled") : colorize12("red", "Disabled"))}`
3290
4704
  );
3291
4705
  if (!isEnabled) {
3292
4706
  consola.info(
@@ -3295,9 +4709,649 @@ Examples:
3295
4709
  }
3296
4710
  });
3297
4711
 
4712
+ // src/cli/commands/upgrade.ts
4713
+ import { Command as Command13, Option as Option10 } from "@commander-js/extra-typings";
4714
+ import { confirm as confirm6 } from "@inquirer/prompts";
4715
+ import { execa as execa4 } from "execa";
4716
+ import {
4717
+ access as access2,
4718
+ copyFile as copyFile2,
4719
+ cp as cp2,
4720
+ mkdir,
4721
+ mkdtemp,
4722
+ readdir as readdir2,
4723
+ readFile as readFile2,
4724
+ rename,
4725
+ rm as rm2,
4726
+ writeFile as writeFile2
4727
+ } from "fs/promises";
4728
+ import { homedir, tmpdir } from "os";
4729
+ import { dirname as dirname5, join as join14, relative as relative2 } from "path";
4730
+ import yoctoSpinner4 from "yocto-spinner";
4731
+ import { z as z17 } from "zod";
4732
+ var CorePackageJson = z17.object({
4733
+ name: z17.string().optional(),
4734
+ version: z17.string(),
4735
+ catalyst: z17.object({ version: z17.string(), ref: z17.string() }).optional()
4736
+ });
4737
+ var DEFAULT_REPOSITORY = "bigcommerce/catalyst";
4738
+ var MOVING_TAGS = /* @__PURE__ */ new Set(["latest", "canary", "alpha"]);
4739
+ var KNOWN_FAMILIES = /* @__PURE__ */ new Set([
4740
+ "@bigcommerce/catalyst-core",
4741
+ "@bigcommerce/catalyst-makeswift",
4742
+ "@bigcommerce/catalyst-b2b-makeswift",
4743
+ "@bigcommerce/catalyst-b2b"
4744
+ ]);
4745
+ var CACHE_DIR = join14(homedir(), ".cache", "catalyst-cli", "cores");
4746
+ var pathExists = (p) => access2(p).then(() => true).catch(() => false);
4747
+ async function listFiles(dir, prefix = "") {
4748
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
4749
+ const nested = await Promise.all(
4750
+ entries.map((entry) => {
4751
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
4752
+ return entry.isDirectory() ? listFiles(join14(dir, entry.name), rel) : Promise.resolve([rel]);
4753
+ })
4754
+ );
4755
+ return nested.flat();
4756
+ }
4757
+ async function filesEqual(a, b) {
4758
+ const [ba, bb] = await Promise.all([
4759
+ readFile2(a).catch(() => null),
4760
+ readFile2(b).catch(() => null)
4761
+ ]);
4762
+ return ba !== null && bb !== null && ba.equals(bb);
4763
+ }
4764
+ async function computeBaseSimilarity(baseDir, destDir) {
4765
+ const baseFiles = await listFiles(baseDir);
4766
+ if (baseFiles.length === 0) return 0;
4767
+ const matches = await Promise.all(
4768
+ baseFiles.map((rel) => filesEqual(join14(baseDir, rel), join14(destDir, rel)))
4769
+ );
4770
+ return matches.filter(Boolean).length / baseFiles.length;
4771
+ }
4772
+ async function isBinary(p) {
4773
+ const buf = await readFile2(p).catch(() => null);
4774
+ if (!buf) return false;
4775
+ return buf.subarray(0, 8e3).includes(0);
4776
+ }
4777
+ async function copyInto(src, dest) {
4778
+ await mkdir(dirname5(dest), { recursive: true });
4779
+ await copyFile2(src, dest);
4780
+ }
4781
+ function parseRef(ref) {
4782
+ const lastAt = ref.lastIndexOf("@");
4783
+ if (lastAt <= 0) throw new Error(`Cannot parse ref "${ref}" \u2014 expected <package>@<version>.`);
4784
+ return { packageName: ref.slice(0, lastAt), version: ref.slice(lastAt + 1) };
4785
+ }
4786
+ async function readResolvedVersion(coreDir) {
4787
+ const raw = await readFile2(join14(coreDir, "package.json"), "utf-8");
4788
+ const pkg = CorePackageJson.parse(JSON.parse(raw));
4789
+ return pkg.catalyst?.version ?? pkg.version;
4790
+ }
4791
+ async function mergeFile(oursPath, basePath, theirsPath) {
4792
+ const merged = await execa4(
4793
+ "git",
4794
+ [
4795
+ "merge-file",
4796
+ "-p",
4797
+ "-L",
4798
+ "ours",
4799
+ "-L",
4800
+ "base",
4801
+ "-L",
4802
+ "theirs",
4803
+ oursPath,
4804
+ basePath,
4805
+ theirsPath
4806
+ ],
4807
+ { reject: false, stripFinalNewline: false }
4808
+ );
4809
+ if (merged.stderr) throw new Error(`git merge-file failed: ${merged.stderr}`);
4810
+ await writeFile2(oursPath, merged.stdout);
4811
+ return (merged.exitCode ?? 0) > 0;
4812
+ }
4813
+ async function mergeModified(rel, baseDir, theirsDir, catalystRoot) {
4814
+ const basePath = join14(baseDir, rel);
4815
+ const theirsPath = join14(theirsDir, rel);
4816
+ const oursPath = join14(catalystRoot, rel);
4817
+ if (await filesEqual(basePath, theirsPath)) return null;
4818
+ if (!await pathExists(oursPath)) {
4819
+ await copyInto(theirsPath, oursPath);
4820
+ return "conflicted";
4821
+ }
4822
+ if (await isBinary(basePath) || await isBinary(theirsPath) || await isBinary(oursPath)) {
4823
+ if (await filesEqual(oursPath, basePath)) {
4824
+ await copyInto(theirsPath, oursPath);
4825
+ return "applied";
4826
+ }
4827
+ return "conflicted";
4828
+ }
4829
+ return await mergeFile(oursPath, basePath, theirsPath) ? "conflicted" : "applied";
4830
+ }
4831
+ async function mergeAdded(rel, theirsDir, catalystRoot, emptyFile) {
4832
+ const theirsPath = join14(theirsDir, rel);
4833
+ const oursPath = join14(catalystRoot, rel);
4834
+ if (!await pathExists(oursPath)) {
4835
+ await copyInto(theirsPath, oursPath);
4836
+ return "added";
4837
+ }
4838
+ if (await filesEqual(oursPath, theirsPath)) return "applied";
4839
+ if (await isBinary(oursPath) || await isBinary(theirsPath)) return "conflicted";
4840
+ const hadConflict = await mergeFile(oursPath, emptyFile, theirsPath);
4841
+ return hadConflict ? "conflicted" : "applied";
4842
+ }
4843
+ async function mergeDeleted(rel, baseDir, catalystRoot) {
4844
+ const basePath = join14(baseDir, rel);
4845
+ const oursPath = join14(catalystRoot, rel);
4846
+ if (!await pathExists(oursPath)) return null;
4847
+ if (await filesEqual(oursPath, basePath)) {
4848
+ await rm2(oursPath, { force: true });
4849
+ return "deleted";
4850
+ }
4851
+ return "conflicted";
4852
+ }
4853
+ async function mergeCorePerFile(baseDir, theirsDir, catalystRoot, emptyFile) {
4854
+ const baseFiles = new Set(await listFiles(baseDir));
4855
+ const theirsFiles = new Set(await listFiles(theirsDir));
4856
+ const result = { applied: [], added: [], deleted: [], conflicted: [] };
4857
+ const decide = async (rel) => {
4858
+ const inBase = baseFiles.has(rel);
4859
+ const inTheirs = theirsFiles.has(rel);
4860
+ let outcome;
4861
+ if (inBase && inTheirs) {
4862
+ outcome = await mergeModified(rel, baseDir, theirsDir, catalystRoot);
4863
+ } else if (inTheirs) {
4864
+ outcome = await mergeAdded(rel, theirsDir, catalystRoot, emptyFile);
4865
+ } else {
4866
+ outcome = await mergeDeleted(rel, baseDir, catalystRoot);
4867
+ }
4868
+ if (outcome) result[outcome].push(rel);
4869
+ };
4870
+ await Promise.all([.../* @__PURE__ */ new Set([...baseFiles, ...theirsFiles])].map(decide));
4871
+ return result;
4872
+ }
4873
+ async function gitSupportsMergeTree() {
4874
+ try {
4875
+ const { stdout } = await execa4("git", ["--version"]);
4876
+ const match = /(\d+)\.(\d+)/.exec(stdout);
4877
+ if (!match) return false;
4878
+ const [major, minor] = [Number(match[1]), Number(match[2])];
4879
+ return major > 2 || major === 2 && minor >= 38;
4880
+ } catch {
4881
+ return false;
4882
+ }
4883
+ }
4884
+ async function resolveStrategy(strategy) {
4885
+ if (strategy !== "auto") return strategy;
4886
+ if (await gitSupportsMergeTree()) return "tree";
4887
+ consola.warn("git < 2.38 \u2014 using the per-file merge engine (no `git merge-tree`).");
4888
+ return "per-file";
4889
+ }
4890
+ async function mergeCoreTree(baseDir, theirsDir, catalystRoot) {
4891
+ const scratch = await mkdtemp(join14(tmpdir(), "catalyst-merge-"));
4892
+ try {
4893
+ await execa4("git", ["init", "-q", scratch]);
4894
+ await execa4("git", ["config", "core.autocrlf", "false"], {
4895
+ env: { ...process.env, GIT_DIR: join14(scratch, ".git") }
4896
+ });
4897
+ const env2 = {
4898
+ ...process.env,
4899
+ GIT_DIR: join14(scratch, ".git"),
4900
+ GIT_AUTHOR_NAME: "catalyst",
4901
+ GIT_AUTHOR_EMAIL: "catalyst@bigcommerce.com",
4902
+ GIT_COMMITTER_NAME: "catalyst",
4903
+ GIT_COMMITTER_EMAIL: "catalyst@bigcommerce.com"
4904
+ };
4905
+ const writeTree = async (workTree, tag) => {
4906
+ const sideEnv = {
4907
+ ...env2,
4908
+ GIT_WORK_TREE: workTree,
4909
+ GIT_INDEX_FILE: join14(scratch, `index.${tag}`)
4910
+ };
4911
+ await execa4("git", ["add", "-A"], { env: sideEnv });
4912
+ return (await execa4("git", ["write-tree"], { env: sideEnv })).stdout.trim();
4913
+ };
4914
+ const commitTree = (tree, parent) => execa4("git", ["commit-tree", tree, "-m", "x", ...parent ? ["-p", parent] : []], {
4915
+ env: env2
4916
+ }).then((r) => r.stdout.trim());
4917
+ const baseTree = await writeTree(baseDir, "base");
4918
+ const oursTree = await writeTree(catalystRoot, "ours");
4919
+ const theirsTree = await writeTree(theirsDir, "theirs");
4920
+ const baseCommit = await commitTree(baseTree);
4921
+ const [oursCommit, theirsCommit] = await Promise.all([
4922
+ commitTree(oursTree, baseCommit),
4923
+ commitTree(theirsTree, baseCommit)
4924
+ ]);
4925
+ await Promise.all([
4926
+ execa4("git", ["branch", "ours", oursCommit], { env: env2 }),
4927
+ execa4("git", ["branch", "theirs", theirsCommit], { env: env2 })
4928
+ ]);
4929
+ const merge = await execa4(
4930
+ "git",
4931
+ [
4932
+ "merge-tree",
4933
+ "--write-tree",
4934
+ "-z",
4935
+ "--name-only",
4936
+ `--merge-base=${baseCommit}`,
4937
+ "ours",
4938
+ "theirs"
4939
+ ],
4940
+ { env: env2, reject: false }
4941
+ );
4942
+ if ((merge.exitCode ?? 0) > 1)
4943
+ throw new Error(merge.stderr || merge.stdout.slice(0, 500) || "git merge-tree failed");
4944
+ const fields = merge.stdout.split("\0");
4945
+ const mergedTree = fields[0];
4946
+ const afterOid = fields.slice(1);
4947
+ const emptyAt = afterOid.indexOf("");
4948
+ const conflicted = afterOid.slice(0, emptyAt === -1 ? afterOid.length : emptyAt);
4949
+ const conflictedSet = new Set(conflicted);
4950
+ const outEnv = {
4951
+ ...env2,
4952
+ GIT_WORK_TREE: catalystRoot,
4953
+ GIT_INDEX_FILE: join14(scratch, "index.out")
4954
+ };
4955
+ await execa4("git", ["read-tree", mergedTree], { env: outEnv });
4956
+ await execa4("git", ["checkout-index", "-a", "-f"], { env: outEnv });
4957
+ const diff = await execa4(
4958
+ "git",
4959
+ ["diff", "-z", "--no-renames", "--name-status", oursTree, mergedTree],
4960
+ { env: env2 }
4961
+ );
4962
+ const tokens = diff.stdout.split("\0");
4963
+ const changes = [];
4964
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
4965
+ const status = tokens[i];
4966
+ const path = tokens[i + 1];
4967
+ if (status && path && !conflictedSet.has(path)) {
4968
+ changes.push({ status, path });
4969
+ }
4970
+ }
4971
+ const deleted = changes.filter((change) => change.status === "D").map((change) => change.path);
4972
+ await Promise.all(deleted.map((path) => rm2(join14(catalystRoot, path), { force: true })));
4973
+ return {
4974
+ applied: changes.filter((change) => change.status !== "A" && change.status !== "D").map((change) => change.path),
4975
+ added: changes.filter((change) => change.status === "A").map((change) => change.path),
4976
+ deleted,
4977
+ conflicted
4978
+ };
4979
+ } finally {
4980
+ await rm2(scratch, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
4981
+ }
4982
+ }
4983
+ async function applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedPkg) {
4984
+ const toRepoPath = (rel) => relDir === "." ? rel : `${relDir}/${rel}`;
4985
+ const pkgRel = "package.json";
4986
+ const stillConflicted = result.conflicted.filter((rel) => !(stampedPkg && rel === pkgRel));
4987
+ const conflictedRepoPaths = new Set(stillConflicted.map(toRepoPath));
4988
+ const clean = [...result.applied, ...result.added, ...result.deleted].map(toRepoPath);
4989
+ if (stampedPkg) clean.push(toRepoPath(pkgRel));
4990
+ const toStage = [...new Set(clean)].filter((path) => !conflictedRepoPaths.has(path));
4991
+ if (toStage.length) {
4992
+ await execa4("git", ["add", "-A", "--", ...toStage], { cwd: gitRoot });
4993
+ }
4994
+ if (stillConflicted.length === 0) return;
4995
+ const OID = /^[0-9a-f]{40,64}$/;
4996
+ const gitFormat = (await execa4("git", ["rev-parse", "--show-object-format"], { cwd: gitRoot, reject: false })).stdout.trim();
4997
+ const NULL_OID = gitFormat === "sha256" ? "0".repeat(64) : "0".repeat(40);
4998
+ const hashBlob = async (file) => {
4999
+ if (!await pathExists(file)) return null;
5000
+ const oid = (await execa4("git", ["hash-object", "-w", "--", file], { cwd: gitRoot })).stdout.trim();
5001
+ return OID.test(oid) ? oid : null;
5002
+ };
5003
+ const headEntry = async (repoPath) => {
5004
+ const { stdout } = await execa4("git", ["ls-tree", "HEAD", "--", repoPath], {
5005
+ cwd: gitRoot,
5006
+ reject: false
5007
+ });
5008
+ const match = /^(\d{6}) blob ([0-9a-f]+)\t/.exec(stdout);
5009
+ return match ? { mode: match[1], oid: match[2] } : null;
5010
+ };
5011
+ const records = await Promise.all(
5012
+ stillConflicted.map(async (rel) => {
5013
+ const repoPath = toRepoPath(rel);
5014
+ const [base, ours, theirs] = await Promise.all([
5015
+ hashBlob(join14(baseDir, rel)),
5016
+ headEntry(repoPath),
5017
+ hashBlob(join14(theirsDir, rel))
5018
+ ]);
5019
+ return [
5020
+ `0 ${NULL_OID} ${repoPath}`,
5021
+ ...base ? [`100644 ${base} 1 ${repoPath}`] : [],
5022
+ ...ours ? [`${ours.mode} ${ours.oid} 2 ${repoPath}`] : [],
5023
+ ...theirs ? [`100644 ${theirs} 3 ${repoPath}`] : []
5024
+ ].join("\n");
5025
+ })
5026
+ );
5027
+ await execa4("git", ["update-index", "--index-info"], {
5028
+ cwd: gitRoot,
5029
+ input: `${records.join("\n")}
5030
+ `
5031
+ });
5032
+ }
5033
+ async function resolveProject2(cwd) {
5034
+ let gitRoot;
5035
+ try {
5036
+ gitRoot = (await execa4("git", ["rev-parse", "--show-toplevel"], { cwd })).stdout.trim();
5037
+ } catch {
5038
+ return null;
5039
+ }
5040
+ const candidateDirs = [.../* @__PURE__ */ new Set([join14(gitRoot, "core"), gitRoot, join14(cwd, "core"), cwd])];
5041
+ const parsed = (await Promise.all(
5042
+ candidateDirs.map(async (dir) => {
5043
+ const pkgPath = join14(dir, "package.json");
5044
+ const raw = await readFile2(pkgPath, "utf-8").catch(() => null);
5045
+ if (!raw) return null;
5046
+ let pkgJson;
5047
+ try {
5048
+ pkgJson = JSON.parse(raw);
5049
+ } catch {
5050
+ return null;
5051
+ }
5052
+ const result = CorePackageJson.safeParse(pkgJson);
5053
+ return result.success ? { dir, pkgPath, raw, pkg: result.data } : null;
5054
+ })
5055
+ )).filter((entry) => entry !== null);
5056
+ if (parsed.length === 0) return null;
5057
+ 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];
5058
+ return {
5059
+ gitRoot,
5060
+ catalystRoot: chosen.dir,
5061
+ relDir: relative2(gitRoot, chosen.dir) || ".",
5062
+ pkgPath: chosen.pkgPath,
5063
+ rawContent: chosen.raw,
5064
+ pkg: chosen.pkg
5065
+ };
5066
+ }
5067
+ async function downloadCore(repository, ref, destDir, token) {
5068
+ const cacheable = !MOVING_TAGS.has(parseRef(ref).version);
5069
+ const cacheKey = `${repository.replace(/[^a-zA-Z0-9._-]/g, "_")}__${ref.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
5070
+ const cachePath = join14(CACHE_DIR, cacheKey);
5071
+ if (cacheable && await pathExists(cachePath) && await pathExists(join14(cachePath, "package.json"))) {
5072
+ await cp2(cachePath, destDir, { recursive: true });
5073
+ return;
5074
+ }
5075
+ const url = `https://api.github.com/repos/${repository}/tarball/${encodeURIComponent(ref)}`;
5076
+ const headers = {
5077
+ "User-Agent": "catalyst-cli",
5078
+ Accept: "application/vnd.github+json"
5079
+ };
5080
+ if (token) headers.Authorization = `Bearer ${token}`;
5081
+ const res = await fetch(url, { headers });
5082
+ if (!res.ok) {
5083
+ let hint = "";
5084
+ if (res.status === 404) hint = ` \u2014 tag "${ref}" not found in ${repository}`;
5085
+ if (res.status === 403) hint = " \u2014 GitHub rate limit hit; set GITHUB_TOKEN to raise it";
5086
+ throw new Error(`GitHub returned ${res.status} for ${ref}${hint}`);
5087
+ }
5088
+ const tarballPath = `${destDir}.tgz`;
5089
+ const rawDir = `${destDir}.raw`;
5090
+ await writeFile2(tarballPath, Buffer.from(await res.arrayBuffer()));
5091
+ await mkdir(rawDir, { recursive: true });
5092
+ await execa4("tar", ["-xzf", tarballPath, "-C", rawDir, "--strip-components=1"]);
5093
+ const coreDir = join14(rawDir, "core");
5094
+ const sourceDir = await pathExists(coreDir) ? coreDir : rawDir;
5095
+ await rename(sourceDir, destDir);
5096
+ await rm2(tarballPath, { force: true });
5097
+ await rm2(rawDir, { recursive: true, force: true });
5098
+ if (cacheable) {
5099
+ await mkdir(CACHE_DIR, { recursive: true });
5100
+ await cp2(destDir, cachePath, { recursive: true }).catch(() => {
5101
+ });
5102
+ }
5103
+ }
5104
+ async function resolveBaseRef(project2, fromOption, assumeYes) {
5105
+ if (project2.pkg.catalyst?.ref) return project2.pkg.catalyst.ref;
5106
+ if (fromOption) return fromOption;
5107
+ const family = project2.pkg.name && KNOWN_FAMILIES.has(project2.pkg.name) ? project2.pkg.name : "@bigcommerce/catalyst-core";
5108
+ const guess = `${family}@${project2.pkg.version}`;
5109
+ consola.warn(`No \`catalyst.ref\` found in package.json. Inferred base: ${guess}`);
5110
+ if (assumeYes) return guess;
5111
+ const ok = await confirm6({ message: `Upgrade from ${guess}?`, default: true }).catch(() => false);
5112
+ if (!ok) {
5113
+ consola.info("Re-run with `--from <ref>` to set the base version explicitly.");
5114
+ return null;
5115
+ }
5116
+ return guess;
5117
+ }
5118
+ function printSummary(result, relDir, stampedPkg) {
5119
+ const unresolved = result.conflicted.filter((f) => !(stampedPkg && f === "package.json"));
5120
+ const parts = [
5121
+ `${result.applied.length} updated`,
5122
+ `${result.added.length} added`,
5123
+ `${result.deleted.length} removed`
5124
+ ];
5125
+ if (unresolved.length) parts.push(`${unresolved.length} with conflicts`);
5126
+ consola.log(`
5127
+ ${parts.join(", ")}`);
5128
+ if (unresolved.length) {
5129
+ const files = unresolved.map((f) => ` ${f}`).join("\n");
5130
+ consola.warn(
5131
+ `
5132
+ Staged the clean changes. ${unresolved.length} file(s) need conflict resolution (the <<<ours/===/theirs>>> markers):
5133
+ ${files}
5134
+
5135
+ Resolve them, then: git add ${relDir} && git commit`
5136
+ );
5137
+ } else {
5138
+ consola.success("Staged all changes \u2014 review with `git diff --cached`, then commit.");
5139
+ }
5140
+ }
5141
+ var upgrade = new Command13("upgrade").configureHelp({ showGlobalOptions: true }).aliases([
5142
+ "up",
5143
+ // These are hidden aliases
5144
+ "upgrayedd",
5145
+ "ugrad"
5146
+ ]).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(
5147
+ new Option10(
5148
+ "--ref <ref>",
5149
+ "Full git tag to upgrade to, e.g. an integration family like @bigcommerce/catalyst-makeswift@1.7.0. Overrides [version]."
5150
+ )
5151
+ ).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(
5152
+ new Option10(
5153
+ "--strategy <strategy>",
5154
+ "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)"
5155
+ ).choices(["auto", "tree", "per-file"]).default("auto").hideHelp()
5156
+ ).addOption(
5157
+ new Option10("--repository <owner/repo>", "GitHub repository to pull versions from").default(
5158
+ DEFAULT_REPOSITORY
5159
+ )
5160
+ ).addHelpText(
5161
+ "after",
5162
+ `
5163
+ Examples:
5164
+ # Upgrade to the latest Catalyst version
5165
+ $ catalyst upgrade
5166
+
5167
+ # Upgrade to a specific version
5168
+ $ catalyst upgrade 1.8.0
5169
+
5170
+ # Preview what would change without applying
5171
+ $ catalyst upgrade --dry-run
5172
+
5173
+ # Upgrade to an integration family (makeswift, b2b-makeswift, ...)
5174
+ $ catalyst upgrade --ref @bigcommerce/catalyst-makeswift@1.7.0
5175
+
5176
+ Conflicts are written as standard <<<ours/===/theirs>>> markers \u2014 the upgrade
5177
+ never aborts. Versions are git tags on the repository (not npm). Set GITHUB_TOKEN
5178
+ to raise the GitHub API rate limit.`
5179
+ ).action(async (version2, options) => {
5180
+ const project2 = await resolveProject2(process.cwd());
5181
+ if (!project2) {
5182
+ consola.error(
5183
+ "Run `catalyst upgrade` from inside a Catalyst git repository (flat core/ repo or a project containing core/)."
5184
+ );
5185
+ process.exit(1);
5186
+ }
5187
+ const { gitRoot, catalystRoot, relDir, pkgPath } = project2;
5188
+ const baseRef = await resolveBaseRef(project2, options.from, options.yes ?? false);
5189
+ if (!baseRef) process.exit(1);
5190
+ let basePackage;
5191
+ let upstreamRef;
5192
+ let upstreamTagVersion;
5193
+ try {
5194
+ ({ packageName: basePackage } = parseRef(baseRef));
5195
+ upstreamRef = options.ref ?? `${basePackage}@${version2 ?? "latest"}`;
5196
+ ({ version: upstreamTagVersion } = parseRef(upstreamRef));
5197
+ } catch {
5198
+ consola.error(
5199
+ `Invalid ref format \u2014 expected <package>@<version> (e.g. @bigcommerce/catalyst-core@1.7.0). Got: "${baseRef}"`
5200
+ );
5201
+ process.exit(1);
5202
+ }
5203
+ if (upstreamRef === baseRef) {
5204
+ consola.success("Already up to date.");
5205
+ return;
5206
+ }
5207
+ if (!options.dryRun) {
5208
+ const statusArgs = relDir === "." ? [] : ["--", relDir];
5209
+ const status = await execa4("git", ["status", "--porcelain", ...statusArgs], {
5210
+ cwd: gitRoot,
5211
+ reject: false
5212
+ });
5213
+ if (status.stdout.trim()) {
5214
+ const where = relDir === "." ? "The repo" : `${relDir}/`;
5215
+ consola.error(
5216
+ `${where} has uncommitted changes. Commit or stash them before upgrading, so the upgrade is isolated and reversible:
5217
+ git add ${relDir} && git commit -m "snapshot before upgrade"
5218
+ # or: git stash`
5219
+ );
5220
+ process.exit(1);
5221
+ }
5222
+ }
5223
+ if (!project2.pkg.catalyst?.ref && !options.from && !options.dryRun) {
5224
+ const { version: baseVersion } = parseRef(baseRef);
5225
+ const rawPkg = await readFile2(pkgPath, "utf-8");
5226
+ const parsedPkg = z17.record(z17.string(), z17.unknown()).parse(JSON.parse(rawPkg));
5227
+ parsedPkg.catalyst = { version: baseVersion, ref: baseRef };
5228
+ await writeFile2(pkgPath, `${JSON.stringify(parsedPkg, null, 2)}
5229
+ `);
5230
+ await execa4("git", ["add", "--", pkgPath], { cwd: gitRoot });
5231
+ consola.success(`Added catalyst.ref \u2192 ${baseRef}`);
5232
+ }
5233
+ const token = process.env.GITHUB_TOKEN;
5234
+ const tmpDir = await mkdtemp(join14(tmpdir(), "catalyst-upgrade-"));
5235
+ try {
5236
+ const baseDir = join14(tmpDir, "base");
5237
+ const theirsDir = join14(tmpDir, "theirs");
5238
+ const emptyFile = join14(tmpDir, ".empty");
5239
+ await writeFile2(emptyFile, "");
5240
+ const downloadSpinner = yoctoSpinner4().start(`Downloading ${baseRef} and ${upstreamRef}...`);
5241
+ try {
5242
+ await Promise.all([
5243
+ downloadCore(options.repository, baseRef, baseDir, token),
5244
+ downloadCore(options.repository, upstreamRef, theirsDir, token)
5245
+ ]);
5246
+ downloadSpinner.success("Downloaded both versions.");
5247
+ } catch (err) {
5248
+ downloadSpinner.error("Download failed");
5249
+ throw err;
5250
+ }
5251
+ if (!project2.pkg.catalyst?.ref && !options.from) {
5252
+ const similarity = await computeBaseSimilarity(baseDir, catalystRoot);
5253
+ const pct = Math.round(similarity * 100);
5254
+ if (similarity < 0.5) {
5255
+ consola.warn(
5256
+ `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.`
5257
+ );
5258
+ }
5259
+ }
5260
+ const resolvedVersion = MOVING_TAGS.has(upstreamTagVersion) ? await readResolvedVersion(theirsDir) : upstreamTagVersion;
5261
+ const newRef = MOVING_TAGS.has(upstreamTagVersion) ? `${basePackage}@${resolvedVersion}` : upstreamRef;
5262
+ if (options.dryRun) {
5263
+ const diffSpinner = yoctoSpinner4().start("Generating diff...");
5264
+ const diff = await execa4(
5265
+ "git",
5266
+ ["diff", "--no-index", "--binary", "--diff-algorithm=histogram", "base", "theirs"],
5267
+ { cwd: tmpDir, reject: false, stripFinalNewline: false }
5268
+ );
5269
+ if ((diff.exitCode ?? 0) > 1) {
5270
+ diffSpinner.error("Failed to generate diff");
5271
+ throw new Error(diff.stderr);
5272
+ }
5273
+ if (!diff.stdout.trim()) {
5274
+ diffSpinner.success("No differences between versions \u2014 already up to date.");
5275
+ return;
5276
+ }
5277
+ const fileCount = (diff.stdout.match(/^diff --git /gm) ?? []).length;
5278
+ diffSpinner.success(`Diff ready \u2014 ${fileCount} file(s) affected.`);
5279
+ consola.log("\nDiff preview (--dry-run, not applied):\n");
5280
+ consola.log(diff.stdout);
5281
+ return;
5282
+ }
5283
+ const strategy = await resolveStrategy(options.strategy);
5284
+ const mergeSpinner = yoctoSpinner4().start(`Merging changes (${strategy})...`);
5285
+ const result = strategy === "tree" ? await mergeCoreTree(baseDir, theirsDir, catalystRoot) : await mergeCorePerFile(baseDir, theirsDir, catalystRoot, emptyFile);
5286
+ const total = result.applied.length + result.added.length + result.deleted.length + result.conflicted.length;
5287
+ if (total === 0) {
5288
+ mergeSpinner.success("No differences between versions \u2014 already up to date.");
5289
+ return;
5290
+ }
5291
+ if (result.conflicted.length) {
5292
+ mergeSpinner.warning("Merged with conflicts \u2014 resolve the markers, then commit.");
5293
+ } else {
5294
+ mergeSpinner.success("Merged cleanly.");
5295
+ }
5296
+ const patchedRaw = await readFile2(pkgPath, "utf-8");
5297
+ let stampedPkg = false;
5298
+ try {
5299
+ const patchedPkg = z17.record(z17.string(), z17.unknown()).parse(JSON.parse(patchedRaw));
5300
+ patchedPkg.catalyst = { version: resolvedVersion, ref: newRef };
5301
+ await writeFile2(pkgPath, `${JSON.stringify(patchedPkg, null, 2)}
5302
+ `);
5303
+ stampedPkg = true;
5304
+ consola.success(`catalyst.ref updated \u2192 ${newRef}`);
5305
+ } catch {
5306
+ const catalystReplacement = JSON.stringify(
5307
+ { version: resolvedVersion, ref: newRef },
5308
+ null,
5309
+ 2
5310
+ ).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
5311
+ const updated = patchedRaw.replace(
5312
+ /"catalyst":\s*\{[^}]*\}/,
5313
+ () => `"catalyst": ${catalystReplacement}`
5314
+ );
5315
+ if (updated !== patchedRaw) {
5316
+ await writeFile2(pkgPath, updated);
5317
+ consola.success(`catalyst.ref updated \u2192 ${newRef}`);
5318
+ consola.info(
5319
+ "package.json still has conflicts in other sections \u2014 resolve them, then: git add package.json"
5320
+ );
5321
+ } else {
5322
+ consola.warn(
5323
+ `package.json has conflicts \u2014 after resolving, add:
5324
+ "catalyst": { "version": "${resolvedVersion}", "ref": "${newRef}" }`
5325
+ );
5326
+ }
5327
+ }
5328
+ try {
5329
+ await applyIndexState(gitRoot, relDir, baseDir, theirsDir, result, stampedPkg);
5330
+ } catch (err) {
5331
+ consola.warn(
5332
+ `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.`
5333
+ );
5334
+ }
5335
+ const gitVersion = await execa4("git", ["--version"]).then((r) => r.stdout.trim()).catch(() => "unknown");
5336
+ await getTelemetry().track("upgrade", {
5337
+ strategy,
5338
+ gitVersion,
5339
+ dryRun: Boolean(options.dryRun),
5340
+ applied: result.applied.length,
5341
+ added: result.added.length,
5342
+ deleted: result.deleted.length,
5343
+ conflicts: result.conflicted.length,
5344
+ hasConflicts: result.conflicted.length > 0
5345
+ });
5346
+ printSummary(result, relDir, stampedPkg);
5347
+ } finally {
5348
+ await rm2(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
5349
+ }
5350
+ });
5351
+
3298
5352
  // src/cli/commands/version.ts
3299
- import { Command as Command11 } from "commander";
3300
- var version = new Command11("version").configureHelp({ showGlobalOptions: true }).description("Display detailed version information.").addHelpText(
5353
+ import { Command as Command14 } from "commander";
5354
+ var version = new Command14("version").configureHelp({ showGlobalOptions: true }).description("Display detailed version information.").addHelpText(
3301
5355
  "after",
3302
5356
  `
3303
5357
  Example:
@@ -3358,41 +5412,15 @@ var telemetryPostHook = async () => {
3358
5412
  };
3359
5413
 
3360
5414
  // src/cli/program.ts
3361
- var defaultEnvPath = resolve(process.cwd(), ".env.local");
3362
- if (existsSync5(defaultEnvPath)) {
3363
- config({ path: defaultEnvPath });
3364
- }
3365
5415
  if (!process.env.CATALYST_STORE_HASH && process.env.BIGCOMMERCE_STORE_HASH) {
3366
5416
  process.env.CATALYST_STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;
3367
5417
  }
3368
- var program = new Command12();
3369
- consola.log(colorize9("cyanBright", `\u25E2 ${package_default.name} v${package_default.version}
5418
+ var program = new Command15();
5419
+ consola.log(colorize13("cyanBright", `\u25E2 ${package_default.name} v${package_default.version}
3370
5420
  `));
3371
5421
  program.name(package_default.name).version(package_default.version).summary("CLI tool for Catalyst development").description(
3372
- "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."
3373
- ).configureHelp({ showGlobalOptions: true }).addOption(
3374
- new Option9(
3375
- "--env-path <path>",
3376
- "Path to environment file to load (relative to current working directory)"
3377
- // We are using argParser, because commander loads in environment variables before executing hooks.
3378
- ).argParser((value) => {
3379
- if (value) {
3380
- const envFilePath = resolve(process.cwd(), value);
3381
- const result = config({
3382
- path: envFilePath,
3383
- override: true
3384
- });
3385
- if (result.error) {
3386
- const errCode = "code" in result.error && typeof result.error.code === "string" ? result.error.code : void 0;
3387
- const message = errCode === "ENOENT" ? `Env file not found: ${envFilePath}` : `Failed to load --env-path ${value}: ${result.error.message}`;
3388
- throw new Error(message);
3389
- }
3390
- consola.log(colorize9("cyanBright", `Loaded environment variables from ${envFilePath}
3391
- `));
3392
- }
3393
- return value;
3394
- })
3395
- ).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);
5422
+ "CLI tool for Catalyst development.\n\nConfiguration priority: flags > process.env > .bigcommerce/project.json. `build` and `deploy` additionally load env files (--env-path, or an auto-loaded .env.local/.env) for the build.\n\nCATALYST_STORE_HASH falls back to BIGCOMMERCE_STORE_HASH if unset.\n\nRun `catalyst <command> --help` for details on a specific command."
5423
+ ).configureHelp({ showGlobalOptions: true }).addCommand(version).addCommand(create2).addCommand(start).addCommand(build).addCommand(deploy).addCommand(domains).addCommand(logs).addCommand(project).addCommand(env).addCommand(channel).addCommand(auth).addCommand(upgrade).addCommand(telemetry2).addCommand(debug).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
3396
5424
 
3397
5425
  // src/cli/index.ts
3398
5426
  var handleFatalError = async (error) => {
@@ -3409,7 +5437,7 @@ var handleFatalError = async (error) => {
3409
5437
  await telemetry3.analytics.closeAndFlush();
3410
5438
  } catch {
3411
5439
  }
3412
- if (error instanceof UnauthorizedError) {
5440
+ if (error instanceof UserActionableError) {
3413
5441
  consola.error(errorMessage);
3414
5442
  process.exit(1);
3415
5443
  }