@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/README.md +1 -0
- package/dist/cli.js +2719 -691
- package/dist/cli.js.map +1 -1
- package/package.json +9 -1
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli/lib/
|
|
4
|
-
var
|
|
5
|
-
constructor() {
|
|
6
|
-
super(
|
|
7
|
-
|
|
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.
|
|
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: "
|
|
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 {
|
|
199
|
-
import {
|
|
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
|
|
207
|
-
import { z as
|
|
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
|
|
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 =
|
|
228
|
-
device_code:
|
|
229
|
-
user_code:
|
|
230
|
-
verification_uri:
|
|
231
|
-
expires_in:
|
|
232
|
-
interval:
|
|
233
|
-
});
|
|
234
|
-
var DeviceCodeSuccessSchema =
|
|
235
|
-
access_token:
|
|
236
|
-
store_hash:
|
|
237
|
-
context:
|
|
238
|
-
api_uri:
|
|
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
|
|
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 =
|
|
289
|
-
data:
|
|
290
|
-
store_name:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
382
|
-
var InfrastructureProjectValidationError = class extends
|
|
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 =
|
|
389
|
-
data:
|
|
390
|
-
|
|
391
|
-
uuid:
|
|
392
|
-
name:
|
|
393
|
-
deployment_hostnames:
|
|
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
|
|
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
|
|
426
|
-
"Infrastructure Projects API not enabled. If you are part of the
|
|
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
|
|
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 =
|
|
437
|
-
data:
|
|
438
|
-
uuid:
|
|
439
|
-
name:
|
|
440
|
-
date_created:
|
|
441
|
-
date_modified:
|
|
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 =
|
|
445
|
-
title:
|
|
446
|
-
detail:
|
|
447
|
-
errors:
|
|
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
|
|
477
|
-
"Infrastructure Projects API not enabled. If you are part of the
|
|
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
|
|
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
|
|
498
|
-
"Infrastructure Projects API not enabled. If you are part of the
|
|
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
|
|
578
|
+
throw new UserActionableError(`Project ${projectUuid} not found.`);
|
|
503
579
|
}
|
|
504
580
|
if (!response.ok) {
|
|
505
|
-
throw
|
|
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(
|
|
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 =
|
|
564
|
-
data:
|
|
565
|
-
store_name:
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
749
|
-
var projectJsonSchema =
|
|
750
|
-
projectUuid:
|
|
845
|
+
import { z as z6 } from "zod";
|
|
846
|
+
var projectJsonSchema = z6.looseObject({
|
|
847
|
+
projectUuid: z6.string().optional()
|
|
751
848
|
});
|
|
752
|
-
var packageJsonSchema =
|
|
753
|
-
dependencies:
|
|
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 =
|
|
768
|
-
const hasProxy =
|
|
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
|
-
|
|
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
|
|
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
|
|
1017
|
+
await execa2(
|
|
883
1018
|
"pnpm",
|
|
884
1019
|
[
|
|
885
1020
|
"dlx",
|
|
886
|
-
`wrangler@${
|
|
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
|
|
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
|
-
).
|
|
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
|
-
|
|
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
|
|
944
|
-
import { Command as Command3, InvalidArgumentError, Option as
|
|
945
|
-
import { colorize as
|
|
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
|
|
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 =
|
|
965
|
-
var channelSchema =
|
|
966
|
-
id:
|
|
967
|
-
name:
|
|
968
|
-
platform:
|
|
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 =
|
|
971
|
-
data:
|
|
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 =
|
|
988
|
-
data:
|
|
989
|
-
storefront_api_token:
|
|
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 =
|
|
994
|
-
data:
|
|
995
|
-
id:
|
|
996
|
-
storefront_api_token:
|
|
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 =
|
|
1001
|
-
data:
|
|
1002
|
-
eligible:
|
|
1003
|
-
message:
|
|
1143
|
+
var eligibilityResponseSchema = z7.object({
|
|
1144
|
+
data: z7.object({
|
|
1145
|
+
eligible: z7.boolean(),
|
|
1146
|
+
message: z7.string()
|
|
1004
1147
|
})
|
|
1005
1148
|
});
|
|
1006
|
-
var channelSiteSchema =
|
|
1007
|
-
data:
|
|
1008
|
-
id:
|
|
1009
|
-
url:
|
|
1010
|
-
channel_id:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1150
|
-
dependencies:
|
|
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, "
|
|
1173
|
-
const middlewarePath = join5(projectDir, "
|
|
1174
|
-
if (!
|
|
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, "
|
|
1181
|
-
if (!
|
|
1297
|
+
const instrumentationPath = join5(projectDir, "instrumentation.ts");
|
|
1298
|
+
if (!existsSync3(instrumentationPath)) return;
|
|
1182
1299
|
if (!process.stdin.isTTY) {
|
|
1183
1300
|
consola.warn(
|
|
1184
|
-
"
|
|
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
|
|
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
|
|
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
|
|
1200
|
-
const corePackageJsonPath = join5(projectDir, "
|
|
1201
|
-
if (
|
|
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
|
|
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, "
|
|
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, "
|
|
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} ${
|
|
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: () =>
|
|
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 = (
|
|
1509
|
-
const separatorIndex =
|
|
1756
|
+
var parseEnvAssignment = (input6) => {
|
|
1757
|
+
const separatorIndex = input6.indexOf("=");
|
|
1510
1758
|
if (separatorIndex === -1) {
|
|
1511
|
-
throw new Error(`Invalid env var format: ${
|
|
1759
|
+
throw new Error(`Invalid env var format: ${input6}. Expected format: KEY=VALUE`);
|
|
1512
1760
|
}
|
|
1513
|
-
const key =
|
|
1514
|
-
const value =
|
|
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: ${
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
2009
|
+
`Linked to channel ${label} and wrote ${colorize5("cyanBright", ".env.local")}.`
|
|
1697
2010
|
);
|
|
1698
2011
|
consola.log(`Next steps:
|
|
1699
2012
|
|
|
1700
|
-
${
|
|
2013
|
+
${colorize5("yellow", "pnpm run dev")}`);
|
|
1701
2014
|
process.exit(0);
|
|
1702
2015
|
});
|
|
1703
|
-
var
|
|
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
|
-
|
|
1706
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
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/
|
|
1746
|
-
|
|
1747
|
-
|
|
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/
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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/
|
|
1781
|
-
import {
|
|
1782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1820
|
-
projectDir
|
|
1821
|
-
ghRef,
|
|
1822
|
-
resetMain = false
|
|
2172
|
+
ref,
|
|
2173
|
+
projectDir
|
|
1823
2174
|
}) => {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
|
1843
|
-
var
|
|
1844
|
-
|
|
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
|
|
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/
|
|
1856
|
-
import {
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
"
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"
|
|
1864
|
-
"
|
|
1865
|
-
"
|
|
1866
|
-
"
|
|
1867
|
-
"
|
|
1868
|
-
"
|
|
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
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
|
1889
|
-
|
|
1890
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
1911
|
-
import { dirname as dirname3, join as
|
|
1912
|
-
import { z as
|
|
1913
|
-
var
|
|
1914
|
-
scripts:
|
|
1915
|
-
|
|
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
|
-
|
|
1919
|
-
|
|
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 =
|
|
1924
|
-
const pkg =
|
|
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.
|
|
1932
|
-
...pkg.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
2427
|
+
"Extract a specific ref (tag, branch, or commit) from the source repository",
|
|
2098
2428
|
"@bigcommerce/catalyst-core@latest"
|
|
2099
|
-
).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
|
|
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
|
|
2442
|
+
new Option4("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
|
|
2113
2443
|
).addOption(
|
|
2114
|
-
new
|
|
2444
|
+
new Option4("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
|
|
2115
2445
|
).addOption(
|
|
2116
|
-
new
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
2584
|
+
const steps = [`cd ${projectName} && ${packageManager} run dev`];
|
|
2253
2585
|
if (useCommerceHosting) {
|
|
2254
2586
|
steps.push(
|
|
2255
|
-
`Run 'cd ${projectName}
|
|
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) => ` ${
|
|
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
|
|
2269
|
-
import { colorize as
|
|
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 {
|
|
2272
|
-
import
|
|
2273
|
-
import { z as
|
|
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 =
|
|
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 =
|
|
2311
|
-
data:
|
|
2312
|
-
upload_url:
|
|
2313
|
-
upload_uuid:
|
|
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 =
|
|
2317
|
-
data:
|
|
2318
|
-
deployment_uuid:
|
|
2909
|
+
var CreateDeploymentSchema = z13.object({
|
|
2910
|
+
data: z13.object({
|
|
2911
|
+
deployment_uuid: z13.uuid()
|
|
2319
2912
|
})
|
|
2320
2913
|
});
|
|
2321
|
-
var ProjectSchema =
|
|
2322
|
-
data:
|
|
2323
|
-
|
|
2324
|
-
uuid:
|
|
2325
|
-
name:
|
|
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 =
|
|
2330
|
-
deployment_uuid:
|
|
2331
|
-
deployment_status:
|
|
2332
|
-
event:
|
|
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:
|
|
2927
|
+
progress: z13.number()
|
|
2335
2928
|
}).nullable(),
|
|
2336
|
-
deployment_hostnames:
|
|
2337
|
-
error:
|
|
2338
|
-
code:
|
|
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 =
|
|
2344
|
-
const distDir =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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: ${
|
|
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
|
|
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
|
|
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
|
-
).
|
|
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
|
|
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 =
|
|
3196
|
+
const projectDir = process.cwd();
|
|
2603
3197
|
await setupCommerceHosting({ projectDir, projectUuid, storeHash, accessToken });
|
|
2604
3198
|
consola.success("Commerce Hosting setup complete.");
|
|
2605
|
-
await
|
|
3199
|
+
const packageManager = await detectProjectPackageManager(projectDir);
|
|
3200
|
+
await installDependencies(projectDir, packageManager);
|
|
2606
3201
|
} else {
|
|
2607
|
-
await cleanupCloudflareIncompatibilities(
|
|
3202
|
+
await cleanupCloudflareIncompatibilities(process.cwd());
|
|
2608
3203
|
}
|
|
2609
3204
|
if (options.prebuilt) {
|
|
2610
|
-
const distDir =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
3244
|
+
apiHost,
|
|
2649
3245
|
environmentVariables
|
|
2650
3246
|
);
|
|
2651
3247
|
const deploymentHostname = await getDeploymentStatus(
|
|
2652
3248
|
deploymentUuid,
|
|
2653
3249
|
storeHash,
|
|
2654
3250
|
accessToken,
|
|
2655
|
-
|
|
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 (!
|
|
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
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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
|
|
4007
|
+
import { Command as Command8 } from "commander";
|
|
2680
4008
|
var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2681
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
4076
|
+
).addCommand(add2).addCommand(remove2).addCommand(list2);
|
|
2749
4077
|
|
|
2750
4078
|
// src/cli/commands/logs.ts
|
|
2751
|
-
import { Command as
|
|
2752
|
-
import { colorize as
|
|
2753
|
-
import { z as
|
|
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
|
|
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 =
|
|
2765
|
-
uuid:
|
|
2766
|
-
project_uuid:
|
|
2767
|
-
request:
|
|
2768
|
-
method:
|
|
2769
|
-
url:
|
|
2770
|
-
status_code:
|
|
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:
|
|
2773
|
-
|
|
2774
|
-
timestamp:
|
|
2775
|
-
level:
|
|
2776
|
-
messages:
|
|
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:
|
|
2780
|
-
timestamp:
|
|
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 =
|
|
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
|
-
|
|
2866
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
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
|
|
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
|
|
2977
|
-
import { Command as
|
|
2978
|
-
import { colorize as
|
|
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 =
|
|
4390
|
+
const projectDir = process.cwd();
|
|
2982
4391
|
if (getProjectState().isTransformed) {
|
|
2983
4392
|
await cleanupCloudflareIncompatibilities(projectDir);
|
|
2984
4393
|
return;
|
|
2985
4394
|
}
|
|
2986
|
-
const shouldSetup = await
|
|
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
|
|
4407
|
+
const packageManager = await detectProjectPackageManager(projectDir);
|
|
4408
|
+
await installDependencies(projectDir, packageManager);
|
|
2999
4409
|
}
|
|
3000
|
-
var
|
|
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,
|
|
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 ? ` ${
|
|
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) => ` ${
|
|
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
|
|
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,
|
|
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
|
|
3063
|
-
const data = await createProject(newProjectName, storeHash, accessToken,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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} ${
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
3200
|
-
import { execa as
|
|
3201
|
-
import { existsSync as
|
|
3202
|
-
import { join as
|
|
3203
|
-
var start = new
|
|
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
|
|
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 =
|
|
3221
|
-
const devVars =
|
|
3222
|
-
if (
|
|
3223
|
-
if (!
|
|
3224
|
-
const target = relative(
|
|
3225
|
-
|
|
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 (!
|
|
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
|
|
4651
|
+
await execa3(
|
|
3238
4652
|
"pnpm",
|
|
3239
4653
|
[
|
|
3240
4654
|
"exec",
|
|
3241
4655
|
"opennextjs-cloudflare",
|
|
3242
4656
|
"preview",
|
|
3243
4657
|
"--config",
|
|
3244
|
-
|
|
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
|
|
3255
|
-
import { colorize as
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
3300
|
-
var version = new
|
|
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
|
|
3369
|
-
consola.log(
|
|
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
|
|
3373
|
-
).configureHelp({ showGlobalOptions: true }).
|
|
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
|
|
5440
|
+
if (error instanceof UserActionableError) {
|
|
3413
5441
|
consola.error(errorMessage);
|
|
3414
5442
|
process.exit(1);
|
|
3415
5443
|
}
|