@bigcommerce/catalyst 1.0.0-alpha.4 → 1.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1169 -571
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli/lib/auth-errors.ts
|
|
4
|
+
var UnauthorizedError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(
|
|
7
|
+
"Your access token is invalid or has expired.\nRun `catalyst auth login` to re-authenticate."
|
|
8
|
+
);
|
|
9
|
+
this.name = "UnauthorizedError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function assertAuthorized(response) {
|
|
13
|
+
if (response.status === 401) {
|
|
14
|
+
throw new UnauthorizedError();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
// src/cli/lib/logger.ts
|
|
4
19
|
import { createConsola } from "consola";
|
|
5
20
|
var consola = createConsola({
|
|
@@ -13,7 +28,7 @@ import { randomBytes, randomUUID } from "crypto";
|
|
|
13
28
|
// package.json
|
|
14
29
|
var package_default = {
|
|
15
30
|
name: "@bigcommerce/catalyst",
|
|
16
|
-
version: "1.0.0-alpha.
|
|
31
|
+
version: "1.0.0-alpha.6",
|
|
17
32
|
type: "module",
|
|
18
33
|
bin: {
|
|
19
34
|
catalyst: "dist/cli.js"
|
|
@@ -113,7 +128,7 @@ var Telemetry = class {
|
|
|
113
128
|
this.correlationId = randomUUID();
|
|
114
129
|
this.startTime = Date.now();
|
|
115
130
|
this.analytics = new Analytics({
|
|
116
|
-
writeKey: "
|
|
131
|
+
writeKey: "not-a-valid-segment-write-key"
|
|
117
132
|
});
|
|
118
133
|
}
|
|
119
134
|
durationMs() {
|
|
@@ -180,19 +195,23 @@ function getTelemetry() {
|
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
// src/cli/program.ts
|
|
183
|
-
import { Option as
|
|
184
|
-
import { Command as
|
|
198
|
+
import { Option as Option9 } from "@commander-js/extra-typings";
|
|
199
|
+
import { Command as Command12 } from "commander";
|
|
185
200
|
import { colorize as colorize9 } from "consola/utils";
|
|
186
201
|
import { config } from "dotenv";
|
|
187
202
|
import { existsSync as existsSync5 } from "fs";
|
|
188
203
|
import { resolve } from "path";
|
|
189
204
|
|
|
190
205
|
// src/cli/commands/auth.ts
|
|
191
|
-
import { Command, Option } from "commander";
|
|
206
|
+
import { Command, Option as Option2 } from "commander";
|
|
207
|
+
import { z as z4 } from "zod";
|
|
208
|
+
|
|
209
|
+
// src/cli/lib/login.ts
|
|
210
|
+
import { confirm, input, password } from "@inquirer/prompts";
|
|
192
211
|
import { colorize } from "consola/utils";
|
|
193
212
|
import open from "open";
|
|
194
213
|
import yoctoSpinner from "yocto-spinner";
|
|
195
|
-
import { z as
|
|
214
|
+
import { z as z2 } from "zod";
|
|
196
215
|
|
|
197
216
|
// src/cli/lib/auth.ts
|
|
198
217
|
import { z } from "zod";
|
|
@@ -201,7 +220,8 @@ var DEVICE_OAUTH_SCOPES = [
|
|
|
201
220
|
"store_v2_information",
|
|
202
221
|
"store_infrastructure_deployments_manage",
|
|
203
222
|
"store_infrastructure_logs_read_only",
|
|
204
|
-
"store_infrastructure_projects_manage"
|
|
223
|
+
"store_infrastructure_projects_manage",
|
|
224
|
+
"store_channel_settings"
|
|
205
225
|
].join(" ");
|
|
206
226
|
var DEFAULT_LOGIN_URL = "https://login.bigcommerce.com";
|
|
207
227
|
var DeviceCodeResponseSchema = z.object({
|
|
@@ -258,19 +278,119 @@ async function waitForDeviceToken(loginUrl, deviceCode, interval) {
|
|
|
258
278
|
return waitForDeviceToken(loginUrl, deviceCode, interval);
|
|
259
279
|
}
|
|
260
280
|
|
|
281
|
+
// src/cli/lib/login.ts
|
|
282
|
+
var LoginAbortedError = class extends Error {
|
|
283
|
+
constructor() {
|
|
284
|
+
super("Login aborted by user.");
|
|
285
|
+
this.name = "LoginAbortedError";
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var StoreProfileSchema = z2.object({
|
|
289
|
+
data: z2.object({
|
|
290
|
+
store_name: z2.string()
|
|
291
|
+
})
|
|
292
|
+
});
|
|
293
|
+
async function fetchStoreProfile(storeHash, accessToken, apiHost) {
|
|
294
|
+
const response = await fetch(`https://${apiHost}/stores/${storeHash}/v3/settings/store/profile`, {
|
|
295
|
+
method: "GET",
|
|
296
|
+
headers: {
|
|
297
|
+
"X-Auth-Token": accessToken,
|
|
298
|
+
Accept: "application/json"
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
303
|
+
}
|
|
304
|
+
const res = await response.json();
|
|
305
|
+
const result = StoreProfileSchema.safeParse(res);
|
|
306
|
+
if (!result.success) {
|
|
307
|
+
throw new Error("Unexpected response from store profile API");
|
|
308
|
+
}
|
|
309
|
+
return result.data.data;
|
|
310
|
+
}
|
|
311
|
+
async function deviceCodeLogin(loginUrl) {
|
|
312
|
+
const deviceCode = await requestDeviceCode(loginUrl);
|
|
313
|
+
consola.info(
|
|
314
|
+
`${colorize("yellow", "Your one-time code:")} ${colorize("bold", deviceCode.user_code)}`
|
|
315
|
+
);
|
|
316
|
+
try {
|
|
317
|
+
await open(deviceCode.verification_uri);
|
|
318
|
+
consola.info(`Opened ${deviceCode.verification_uri} in your browser.`);
|
|
319
|
+
} catch {
|
|
320
|
+
consola.info(`Open ${deviceCode.verification_uri} in your browser and enter the code above.`);
|
|
321
|
+
}
|
|
322
|
+
const spinner = yoctoSpinner().start("Waiting for authentication...");
|
|
323
|
+
const credentials = await waitForDeviceToken(
|
|
324
|
+
loginUrl,
|
|
325
|
+
deviceCode.device_code,
|
|
326
|
+
deviceCode.interval
|
|
327
|
+
);
|
|
328
|
+
spinner.success("Authentication complete.");
|
|
329
|
+
return {
|
|
330
|
+
storeHash: credentials.store_hash,
|
|
331
|
+
accessToken: credentials.access_token
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async function manualLogin(apiHost) {
|
|
335
|
+
consola.info(
|
|
336
|
+
`Create a store-level API account from your BigCommerce Control Panel:
|
|
337
|
+
Settings \u2192 API \u2192 Store-level API accounts \u2192 Create API account
|
|
338
|
+
Grant these OAuth scopes: ${DEVICE_OAUTH_SCOPES}`
|
|
339
|
+
);
|
|
340
|
+
const storeHashInput = await input({ message: "Store hash:" });
|
|
341
|
+
const storeHash = storeHashInput.trim();
|
|
342
|
+
if (!storeHash) {
|
|
343
|
+
throw new Error("Store hash is required.");
|
|
344
|
+
}
|
|
345
|
+
const accessTokenInput = await password({ message: "Access token:", mask: true });
|
|
346
|
+
const accessToken = accessTokenInput.trim();
|
|
347
|
+
if (!accessToken) {
|
|
348
|
+
throw new Error("Access token is required.");
|
|
349
|
+
}
|
|
350
|
+
const spinner = yoctoSpinner().start("Validating credentials...");
|
|
351
|
+
try {
|
|
352
|
+
const profile = await fetchStoreProfile(storeHash, accessToken, apiHost);
|
|
353
|
+
spinner.success(`Validated credentials for ${profile.store_name} (${storeHash}).`);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
spinner.error("Failed to validate credentials.");
|
|
356
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
357
|
+
throw new Error(
|
|
358
|
+
`Could not validate credentials (${message}). Double-check your store hash and access token, then try again.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return { storeHash, accessToken };
|
|
362
|
+
}
|
|
363
|
+
async function login(loginUrl, apiHost) {
|
|
364
|
+
try {
|
|
365
|
+
return await deviceCodeLogin(loginUrl);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
consola.warn(`Browser login didn't work (${message}).`);
|
|
369
|
+
const shouldFallback = await confirm({
|
|
370
|
+
message: "Try logging in manually with a store hash and access token instead?",
|
|
371
|
+
default: true
|
|
372
|
+
});
|
|
373
|
+
if (!shouldFallback) {
|
|
374
|
+
throw new LoginAbortedError();
|
|
375
|
+
}
|
|
376
|
+
return manualLogin(apiHost);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
261
380
|
// src/cli/lib/project.ts
|
|
262
|
-
import { z as
|
|
381
|
+
import { z as z3 } from "zod";
|
|
263
382
|
var InfrastructureProjectValidationError = class extends Error {
|
|
264
383
|
constructor(message) {
|
|
265
384
|
super(message);
|
|
266
385
|
this.name = "InfrastructureProjectValidationError";
|
|
267
386
|
}
|
|
268
387
|
};
|
|
269
|
-
var fetchProjectsSchema =
|
|
270
|
-
data:
|
|
271
|
-
|
|
272
|
-
uuid:
|
|
273
|
-
name:
|
|
388
|
+
var fetchProjectsSchema = z3.object({
|
|
389
|
+
data: z3.array(
|
|
390
|
+
z3.object({
|
|
391
|
+
uuid: z3.string(),
|
|
392
|
+
name: z3.string(),
|
|
393
|
+
deployment_hostnames: z3.array(z3.string())
|
|
274
394
|
})
|
|
275
395
|
)
|
|
276
396
|
});
|
|
@@ -288,6 +408,7 @@ async function hasProjectsAccess(storeHash, accessToken, apiHost) {
|
|
|
288
408
|
method: "GET",
|
|
289
409
|
headers: authHeaders(accessToken)
|
|
290
410
|
});
|
|
411
|
+
assertAuthorized(response);
|
|
291
412
|
if (response.status === 200) return true;
|
|
292
413
|
if (response.status === 403) return false;
|
|
293
414
|
throw new Error(
|
|
@@ -299,6 +420,7 @@ async function fetchProjects(storeHash, accessToken, apiHost) {
|
|
|
299
420
|
method: "GET",
|
|
300
421
|
headers: authHeaders(accessToken)
|
|
301
422
|
});
|
|
423
|
+
assertAuthorized(response);
|
|
302
424
|
if (response.status === 403) {
|
|
303
425
|
throw new Error(
|
|
304
426
|
"Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
|
|
@@ -311,18 +433,18 @@ async function fetchProjects(storeHash, accessToken, apiHost) {
|
|
|
311
433
|
const { data } = fetchProjectsSchema.parse(res);
|
|
312
434
|
return data;
|
|
313
435
|
}
|
|
314
|
-
var createProjectSchema =
|
|
315
|
-
data:
|
|
316
|
-
uuid:
|
|
317
|
-
name:
|
|
318
|
-
date_created:
|
|
319
|
-
date_modified:
|
|
436
|
+
var createProjectSchema = z3.object({
|
|
437
|
+
data: z3.object({
|
|
438
|
+
uuid: z3.string(),
|
|
439
|
+
name: z3.string(),
|
|
440
|
+
date_created: z3.coerce.date(),
|
|
441
|
+
date_modified: z3.coerce.date()
|
|
320
442
|
})
|
|
321
443
|
});
|
|
322
|
-
var validationErrorBodySchema =
|
|
323
|
-
title:
|
|
324
|
-
detail:
|
|
325
|
-
errors:
|
|
444
|
+
var validationErrorBodySchema = z3.object({
|
|
445
|
+
title: z3.string().optional(),
|
|
446
|
+
detail: z3.string().optional(),
|
|
447
|
+
errors: z3.record(z3.string(), z3.string()).optional()
|
|
326
448
|
});
|
|
327
449
|
function extractValidationMessage(body) {
|
|
328
450
|
const parsed = validationErrorBodySchema.safeParse(body);
|
|
@@ -343,6 +465,7 @@ async function createProject(name, storeHash, accessToken, apiHost) {
|
|
|
343
465
|
},
|
|
344
466
|
body: JSON.stringify({ name })
|
|
345
467
|
});
|
|
468
|
+
assertAuthorized(response);
|
|
346
469
|
if (response.status === 400 || response.status === 422) {
|
|
347
470
|
const body = await response.json().catch(() => null);
|
|
348
471
|
const fallback = response.status === 422 ? "The project name you entered doesn't meet the requirements. It must be 3\u201332 characters long and use only letters, numbers, hyphens (-), underscores (_), and periods (.)" : response.statusText;
|
|
@@ -369,6 +492,7 @@ async function deleteProject(projectUuid, storeHash, accessToken, apiHost) {
|
|
|
369
492
|
method: "DELETE",
|
|
370
493
|
headers: authHeaders(accessToken)
|
|
371
494
|
});
|
|
495
|
+
assertAuthorized(response);
|
|
372
496
|
if (response.status === 403) {
|
|
373
497
|
throw new Error(
|
|
374
498
|
"Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
|
|
@@ -398,18 +522,50 @@ function getProjectConfig() {
|
|
|
398
522
|
default: "catalyst"
|
|
399
523
|
},
|
|
400
524
|
storeHash: { type: "string" },
|
|
401
|
-
accessToken: { type: "string" }
|
|
525
|
+
accessToken: { type: "string" },
|
|
526
|
+
env: {
|
|
527
|
+
type: "object",
|
|
528
|
+
additionalProperties: { type: "string" },
|
|
529
|
+
default: {}
|
|
530
|
+
}
|
|
402
531
|
}
|
|
403
532
|
});
|
|
404
533
|
}
|
|
405
534
|
|
|
535
|
+
// src/cli/lib/shared-options.ts
|
|
536
|
+
import { Option } from "commander";
|
|
537
|
+
var storeHashOption = () => new Option(
|
|
538
|
+
"--store-hash <hash>",
|
|
539
|
+
"BigCommerce store hash. Can be found in the URL of your store Control Panel. Read from .bigcommerce/project.json or .env when not provided."
|
|
540
|
+
).env("CATALYST_STORE_HASH");
|
|
541
|
+
var accessTokenOption = () => new Option(
|
|
542
|
+
"--access-token <token>",
|
|
543
|
+
"BigCommerce access token. Can be found after creating a store-level API account. Read from .bigcommerce/project.json or .env when not provided."
|
|
544
|
+
).env("CATALYST_ACCESS_TOKEN");
|
|
545
|
+
var apiHostOption = () => new Option("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com").hideHelp();
|
|
546
|
+
var loginUrlOption = () => new Option("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp();
|
|
547
|
+
var projectUuidOption = () => new Option(
|
|
548
|
+
"--project-uuid <uuid>",
|
|
549
|
+
"BigCommerce infrastructure project UUID. Read from .bigcommerce/project.json or .env when not provided."
|
|
550
|
+
).env("CATALYST_PROJECT_UUID");
|
|
551
|
+
var resolveProjectUuid = (options) => {
|
|
552
|
+
const config2 = getProjectConfig();
|
|
553
|
+
const projectUuid = options.projectUuid ?? config2.get("projectUuid");
|
|
554
|
+
if (!projectUuid) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
"Project UUID is required. Please run either `catalyst project link` or `catalyst project create` or this command again with --project-uuid <uuid>."
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return projectUuid;
|
|
560
|
+
};
|
|
561
|
+
|
|
406
562
|
// src/cli/commands/auth.ts
|
|
407
|
-
var
|
|
408
|
-
data:
|
|
409
|
-
store_name:
|
|
563
|
+
var StoreProfileSchema2 = z4.object({
|
|
564
|
+
data: z4.object({
|
|
565
|
+
store_name: z4.string()
|
|
410
566
|
})
|
|
411
567
|
});
|
|
412
|
-
async function
|
|
568
|
+
async function fetchStoreProfile2(storeHash, accessToken, apiHost) {
|
|
413
569
|
const response = await fetch(`https://${apiHost}/stores/${storeHash}/v3/settings/store/profile`, {
|
|
414
570
|
method: "GET",
|
|
415
571
|
headers: {
|
|
@@ -417,11 +573,12 @@ async function fetchStoreProfile(storeHash, accessToken, apiHost) {
|
|
|
417
573
|
Accept: "application/json"
|
|
418
574
|
}
|
|
419
575
|
});
|
|
576
|
+
assertAuthorized(response);
|
|
420
577
|
if (!response.ok) {
|
|
421
578
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
422
579
|
}
|
|
423
580
|
const res = await response.json();
|
|
424
|
-
const result =
|
|
581
|
+
const result = StoreProfileSchema2.safeParse(res);
|
|
425
582
|
if (!result.success) {
|
|
426
583
|
throw new Error("Unexpected response from store profile API");
|
|
427
584
|
}
|
|
@@ -435,17 +592,17 @@ Example:
|
|
|
435
592
|
|
|
436
593
|
Logged in to My Store (abc123), connected to project my-project (43eba682-0c48-11f1-9bd5-827a48b0ce1e)`
|
|
437
594
|
).addOption(
|
|
438
|
-
new
|
|
595
|
+
new Option2(
|
|
439
596
|
"--store-hash <hash>",
|
|
440
597
|
"BigCommerce store hash. Can be found in the URL of your store Control Panel."
|
|
441
598
|
).env("CATALYST_STORE_HASH")
|
|
442
599
|
).addOption(
|
|
443
|
-
new
|
|
600
|
+
new Option2(
|
|
444
601
|
"--access-token <token>",
|
|
445
602
|
"BigCommerce access token. Can be found after creating a store-level API account."
|
|
446
603
|
).env("CATALYST_ACCESS_TOKEN")
|
|
447
604
|
).addOption(
|
|
448
|
-
new
|
|
605
|
+
new Option2("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com").hideHelp()
|
|
449
606
|
).action(async (options) => {
|
|
450
607
|
try {
|
|
451
608
|
const config2 = getProjectConfig();
|
|
@@ -459,7 +616,7 @@ Example:
|
|
|
459
616
|
process.exit(1);
|
|
460
617
|
return;
|
|
461
618
|
}
|
|
462
|
-
const store = await
|
|
619
|
+
const store = await fetchStoreProfile2(storeHash, accessToken, options.apiHost);
|
|
463
620
|
const projectUuid = config2.get("projectUuid");
|
|
464
621
|
if (projectUuid) {
|
|
465
622
|
const projects = await fetchProjects(storeHash, accessToken, options.apiHost);
|
|
@@ -479,7 +636,11 @@ Example:
|
|
|
479
636
|
process.exit(0);
|
|
480
637
|
} catch (error) {
|
|
481
638
|
const message = error instanceof Error ? error.message : String(error);
|
|
482
|
-
if (
|
|
639
|
+
if (error instanceof UnauthorizedError) {
|
|
640
|
+
consola.error(
|
|
641
|
+
"Not logged in: your access token is invalid or has expired. Run `catalyst auth login`."
|
|
642
|
+
);
|
|
643
|
+
} else if (message.includes("401") || message.includes("403")) {
|
|
483
644
|
consola.error(`Not logged in: invalid credentials (${message})`);
|
|
484
645
|
} else {
|
|
485
646
|
consola.error(`Failed to verify credentials: ${message}`);
|
|
@@ -487,30 +648,30 @@ Example:
|
|
|
487
648
|
process.exit(1);
|
|
488
649
|
}
|
|
489
650
|
});
|
|
490
|
-
var
|
|
491
|
-
"Authenticate via browser using the OAuth device code flow. If already logged in, displays current credentials and suggests running `catalyst auth logout` to re-authenticate."
|
|
651
|
+
var login2 = new Command("login").configureHelp({ showGlobalOptions: true }).description(
|
|
652
|
+
"Authenticate via browser using the OAuth device code flow. Falls back to an interactive store hash + access token prompt if the browser flow is unavailable. If already logged in, displays current credentials and suggests running `catalyst auth logout` to re-authenticate."
|
|
492
653
|
).addHelpText(
|
|
493
654
|
"after",
|
|
494
655
|
`
|
|
495
656
|
Examples:
|
|
496
|
-
# Login
|
|
657
|
+
# Login interactively (browser, with manual fallback)
|
|
497
658
|
$ catalyst auth login
|
|
498
659
|
|
|
499
|
-
# Login with existing credentials (skips
|
|
660
|
+
# Login with existing credentials (skips interactive flow)
|
|
500
661
|
$ catalyst auth login --store-hash <STORE_HASH> --access-token <ACCESS_TOKEN>`
|
|
501
662
|
).addOption(
|
|
502
|
-
new
|
|
663
|
+
new Option2(
|
|
503
664
|
"--store-hash <hash>",
|
|
504
665
|
"BigCommerce store hash. Can be found in the URL of your store Control Panel."
|
|
505
666
|
).env("CATALYST_STORE_HASH")
|
|
506
667
|
).addOption(
|
|
507
|
-
new
|
|
668
|
+
new Option2(
|
|
508
669
|
"--access-token <token>",
|
|
509
670
|
"BigCommerce access token. Can be found after creating a store-level API account."
|
|
510
671
|
).env("CATALYST_ACCESS_TOKEN")
|
|
511
672
|
).addOption(
|
|
512
|
-
new
|
|
513
|
-
).action(async (options) => {
|
|
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) => {
|
|
514
675
|
try {
|
|
515
676
|
const config2 = getProjectConfig();
|
|
516
677
|
const storeHash = options.storeHash ?? config2.get("storeHash");
|
|
@@ -521,30 +682,19 @@ Examples:
|
|
|
521
682
|
process.exit(0);
|
|
522
683
|
return;
|
|
523
684
|
}
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
} catch {
|
|
685
|
+
const credentials = await login(options.loginUrl, options.apiHost);
|
|
686
|
+
config2.set("storeHash", credentials.storeHash);
|
|
687
|
+
config2.set("accessToken", credentials.accessToken);
|
|
688
|
+
consola.success(`Logged in to store ${credentials.storeHash}.`);
|
|
689
|
+
process.exit(0);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (error instanceof LoginAbortedError) {
|
|
532
692
|
consola.info(
|
|
533
|
-
|
|
693
|
+
"Login aborted. Re-run `catalyst auth login` when you have your credentials ready."
|
|
534
694
|
);
|
|
695
|
+
process.exit(0);
|
|
696
|
+
return;
|
|
535
697
|
}
|
|
536
|
-
const spinner = yoctoSpinner().start("Waiting for authentication...");
|
|
537
|
-
const credentials = await waitForDeviceToken(
|
|
538
|
-
options.loginUrl,
|
|
539
|
-
deviceCode.device_code,
|
|
540
|
-
deviceCode.interval
|
|
541
|
-
);
|
|
542
|
-
spinner.success("Authentication complete.");
|
|
543
|
-
config2.set("storeHash", credentials.store_hash);
|
|
544
|
-
config2.set("accessToken", credentials.access_token);
|
|
545
|
-
consola.success(`Logged in to store ${credentials.store_hash}.`);
|
|
546
|
-
process.exit(0);
|
|
547
|
-
} catch (error) {
|
|
548
698
|
const message = error instanceof Error ? error.message : String(error);
|
|
549
699
|
consola.error(`Login failed: ${message}`);
|
|
550
700
|
process.exit(1);
|
|
@@ -575,12 +725,12 @@ Example:
|
|
|
575
725
|
process.exit(1);
|
|
576
726
|
}
|
|
577
727
|
});
|
|
578
|
-
var auth = new Command("auth").configureHelp({ showGlobalOptions: true }).description("Manage authentication for the BigCommerce CLI.").addCommand(whoami).addCommand(
|
|
728
|
+
var auth = new Command("auth").configureHelp({ showGlobalOptions: true }).description("Manage authentication for the BigCommerce CLI.").addCommand(whoami).addCommand(login2).addCommand(logout);
|
|
579
729
|
|
|
580
730
|
// src/cli/commands/build.ts
|
|
581
|
-
import { Command as Command2, Option as
|
|
731
|
+
import { Command as Command2, Option as Option3 } from "commander";
|
|
582
732
|
import { execa } from "execa";
|
|
583
|
-
import { copyFile, cp, writeFile } from "fs/promises";
|
|
733
|
+
import { copyFile, cp, rm, writeFile } from "fs/promises";
|
|
584
734
|
import { join as join4 } from "path";
|
|
585
735
|
|
|
586
736
|
// src/cli/lib/get-module-cli-path.ts
|
|
@@ -595,12 +745,12 @@ function getModuleCliPath() {
|
|
|
595
745
|
// src/cli/lib/project-state.ts
|
|
596
746
|
import { existsSync, readFileSync } from "fs";
|
|
597
747
|
import { join as join3 } from "path";
|
|
598
|
-
import { z as
|
|
599
|
-
var projectJsonSchema =
|
|
600
|
-
projectUuid:
|
|
748
|
+
import { z as z5 } from "zod";
|
|
749
|
+
var projectJsonSchema = z5.looseObject({
|
|
750
|
+
projectUuid: z5.string().optional()
|
|
601
751
|
});
|
|
602
|
-
var packageJsonSchema =
|
|
603
|
-
dependencies:
|
|
752
|
+
var packageJsonSchema = z5.looseObject({
|
|
753
|
+
dependencies: z5.record(z5.string(), z5.string()).optional()
|
|
604
754
|
});
|
|
605
755
|
var safeReadJson = (path) => {
|
|
606
756
|
try {
|
|
@@ -633,12 +783,17 @@ function getProjectState(cwd = process.cwd()) {
|
|
|
633
783
|
}
|
|
634
784
|
|
|
635
785
|
// src/cli/lib/wrangler-config.ts
|
|
786
|
+
function getCompatibilityDate(now = /* @__PURE__ */ new Date()) {
|
|
787
|
+
const date = new Date(now);
|
|
788
|
+
date.setUTCMonth(date.getUTCMonth() - 1);
|
|
789
|
+
return date.toISOString().slice(0, 10);
|
|
790
|
+
}
|
|
636
791
|
function getWranglerConfig(projectUuid) {
|
|
637
792
|
return {
|
|
638
793
|
$schema: "node_modules/wrangler/config-schema.json",
|
|
639
794
|
main: "../.open-next/worker.js",
|
|
640
795
|
name: `project-${projectUuid}`,
|
|
641
|
-
compatibility_date:
|
|
796
|
+
compatibility_date: getCompatibilityDate(),
|
|
642
797
|
compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"],
|
|
643
798
|
observability: {
|
|
644
799
|
enabled: true,
|
|
@@ -697,6 +852,7 @@ async function buildCatalystProject(projectUuid) {
|
|
|
697
852
|
const openNextOutDir = join4(coreDir, ".open-next");
|
|
698
853
|
const bigcommerceDistDir = join4(coreDir, ".bigcommerce", "dist");
|
|
699
854
|
const wranglerConfig = getWranglerConfig(projectUuid);
|
|
855
|
+
await rm(bigcommerceDistDir, { recursive: true, force: true });
|
|
700
856
|
consola.start("Copying templates...");
|
|
701
857
|
await copyFile(
|
|
702
858
|
join4(getModuleCliPath(), "templates", "open-next.config.ts"),
|
|
@@ -758,7 +914,7 @@ Examples:
|
|
|
758
914
|
# Include project UUID
|
|
759
915
|
$ catalyst build --project-uuid <UUID>`
|
|
760
916
|
).addOption(
|
|
761
|
-
new
|
|
917
|
+
new Option3(
|
|
762
918
|
"--project-uuid <uuid>",
|
|
763
919
|
"Project UUID to be included in the deployment configuration."
|
|
764
920
|
).env("CATALYST_PROJECT_UUID")
|
|
@@ -783,42 +939,18 @@ Examples:
|
|
|
783
939
|
await buildCatalystProject(projectUuid);
|
|
784
940
|
});
|
|
785
941
|
|
|
786
|
-
// src/cli/commands/
|
|
787
|
-
import {
|
|
788
|
-
import {
|
|
789
|
-
import {
|
|
790
|
-
import {
|
|
791
|
-
import {
|
|
792
|
-
import kebabCase from "lodash.kebabcase";
|
|
793
|
-
import { join as join9 } from "path";
|
|
942
|
+
// src/cli/commands/channel.ts
|
|
943
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
944
|
+
import { Command as Command3, InvalidArgumentError, Option as Option4 } from "commander";
|
|
945
|
+
import { colorize as colorize3 } from "consola/utils";
|
|
946
|
+
import { outputFileSync } from "fs-extra/esm";
|
|
947
|
+
import { join as join6 } from "path";
|
|
794
948
|
|
|
795
|
-
// src/cli/lib/
|
|
796
|
-
import {
|
|
797
|
-
import { existsSync as existsSync2 } from "fs";
|
|
798
|
-
import { join as join5 } from "path";
|
|
799
|
-
import yoctoSpinner2 from "yocto-spinner";
|
|
800
|
-
var WORKSPACE_PACKAGES = ["packages/catalyst", "packages/client"];
|
|
801
|
-
var buildWorkspacePackages = (projectDir) => {
|
|
802
|
-
const hasCore = existsSync2(join5(projectDir, "core"));
|
|
803
|
-
const hasAllWorkspacePackages = WORKSPACE_PACKAGES.every(
|
|
804
|
-
(pkg) => existsSync2(join5(projectDir, pkg))
|
|
805
|
-
);
|
|
806
|
-
if (!hasCore || !hasAllWorkspacePackages) return;
|
|
807
|
-
WORKSPACE_PACKAGES.forEach((pkg) => {
|
|
808
|
-
const spinner = yoctoSpinner2().start(`Building ${pkg}...`);
|
|
809
|
-
try {
|
|
810
|
-
execSync("pnpm build", { cwd: join5(projectDir, pkg), stdio: "ignore" });
|
|
811
|
-
spinner.success(`Built ${pkg}.`);
|
|
812
|
-
} catch (error) {
|
|
813
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
814
|
-
spinner.error(`Failed to build ${pkg}: ${message}`);
|
|
815
|
-
throw error;
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
};
|
|
949
|
+
// src/cli/lib/channel-site-flow.ts
|
|
950
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
819
951
|
|
|
820
952
|
// src/cli/lib/channels.ts
|
|
821
|
-
import { z as
|
|
953
|
+
import { z as z6 } from "zod";
|
|
822
954
|
var cliApiUrl = (origin, storeHash, path) => `${origin}/stores/${storeHash}/cli-api/v3${path}`;
|
|
823
955
|
var channelsUrl = (storeHash, apiHost, query2 = {}) => {
|
|
824
956
|
const params = new URLSearchParams(query2).toString();
|
|
@@ -829,32 +961,53 @@ var authHeaders2 = (accessToken) => ({
|
|
|
829
961
|
"X-Correlation-Id": getTelemetry().correlationId,
|
|
830
962
|
Accept: "application/json"
|
|
831
963
|
});
|
|
832
|
-
var envVarsSchema =
|
|
833
|
-
var channelSchema =
|
|
834
|
-
id:
|
|
835
|
-
name:
|
|
836
|
-
platform:
|
|
964
|
+
var envVarsSchema = z6.record(z6.string(), z6.coerce.string());
|
|
965
|
+
var channelSchema = z6.object({
|
|
966
|
+
id: z6.number(),
|
|
967
|
+
name: z6.string(),
|
|
968
|
+
platform: z6.string()
|
|
837
969
|
});
|
|
838
|
-
var channelsResponseSchema =
|
|
839
|
-
data:
|
|
970
|
+
var channelsResponseSchema = z6.object({
|
|
971
|
+
data: z6.array(channelSchema)
|
|
840
972
|
});
|
|
841
|
-
var
|
|
842
|
-
|
|
843
|
-
|
|
973
|
+
var CHANNEL_PLATFORM_ORDER = ["catalyst", "next", "bigcommerce"];
|
|
974
|
+
function sortChannelsByPlatform(channels) {
|
|
975
|
+
return [...channels].sort((a, b) => {
|
|
976
|
+
const aIndex = CHANNEL_PLATFORM_ORDER.indexOf(a.platform);
|
|
977
|
+
const bIndex = CHANNEL_PLATFORM_ORDER.indexOf(b.platform);
|
|
978
|
+
if (aIndex === -1 && bIndex === -1) return 0;
|
|
979
|
+
if (aIndex === -1) return 1;
|
|
980
|
+
if (bIndex === -1) return -1;
|
|
981
|
+
return aIndex - bIndex;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function channelPlatformLabel(platform) {
|
|
985
|
+
return platform === "bigcommerce" ? "Stencil" : platform.charAt(0).toUpperCase() + platform.slice(1);
|
|
986
|
+
}
|
|
987
|
+
var initResponseSchema = z6.object({
|
|
988
|
+
data: z6.object({
|
|
989
|
+
storefront_api_token: z6.string(),
|
|
844
990
|
envVars: envVarsSchema
|
|
845
991
|
})
|
|
846
992
|
});
|
|
847
|
-
var createChannelResponseSchema =
|
|
848
|
-
data:
|
|
849
|
-
id:
|
|
850
|
-
storefront_api_token:
|
|
993
|
+
var createChannelResponseSchema = z6.object({
|
|
994
|
+
data: z6.object({
|
|
995
|
+
id: z6.number(),
|
|
996
|
+
storefront_api_token: z6.string(),
|
|
851
997
|
envVars: envVarsSchema
|
|
852
998
|
})
|
|
853
999
|
});
|
|
854
|
-
var eligibilityResponseSchema =
|
|
855
|
-
data:
|
|
856
|
-
eligible:
|
|
857
|
-
message:
|
|
1000
|
+
var eligibilityResponseSchema = z6.object({
|
|
1001
|
+
data: z6.object({
|
|
1002
|
+
eligible: z6.boolean(),
|
|
1003
|
+
message: z6.string()
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
var channelSiteSchema = z6.object({
|
|
1007
|
+
data: z6.object({
|
|
1008
|
+
id: z6.number(),
|
|
1009
|
+
url: z6.string(),
|
|
1010
|
+
channel_id: z6.number()
|
|
858
1011
|
})
|
|
859
1012
|
});
|
|
860
1013
|
async function getChannelInit(channelId, storeHash, accessToken, origin) {
|
|
@@ -917,118 +1070,44 @@ async function fetchAvailableChannels(storeHash, accessToken, apiHost) {
|
|
|
917
1070
|
}
|
|
918
1071
|
}
|
|
919
1072
|
);
|
|
1073
|
+
assertAuthorized(response);
|
|
920
1074
|
if (!response.ok) {
|
|
921
1075
|
throw new Error(`GET /v3/channels failed: ${response.status} ${response.statusText}`);
|
|
922
1076
|
}
|
|
923
1077
|
return channelsResponseSchema.parse(await response.json()).data;
|
|
924
1078
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// src/cli/lib/checkout-ref.ts
|
|
938
|
-
function checkoutRef(repoDir, ref) {
|
|
939
|
-
try {
|
|
940
|
-
const spawn = spawnSync("git", ["checkout", ref, "--"], {
|
|
941
|
-
cwd: repoDir,
|
|
942
|
-
encoding: "utf8",
|
|
943
|
-
shell: false
|
|
944
|
-
});
|
|
945
|
-
const stderr = spawn.stderr.trim();
|
|
946
|
-
if (spawn.status !== 0 && stderr) {
|
|
947
|
-
throw new Error(stderr);
|
|
948
|
-
}
|
|
949
|
-
consola.success(`Checked out ref ${ref} successfully.`);
|
|
950
|
-
} catch (error) {
|
|
951
|
-
if (isExecException(error)) {
|
|
952
|
-
const stderr = error.stderr ? error.stderr.toString() : "";
|
|
953
|
-
if (stderr.includes(`fatal: reference is not a tree: ${ref}`) || stderr.includes(`fatal: ambiguous argument '${ref}'`) || stderr.includes(`unknown revision or path not in the working tree`)) {
|
|
954
|
-
consola.error(`Ref '${ref}' not found in the repository.`);
|
|
955
|
-
} else {
|
|
956
|
-
consola.error(`Error checking out ref '${ref}':`, stderr.trim());
|
|
957
|
-
}
|
|
958
|
-
} else if (error instanceof Error) {
|
|
959
|
-
consola.error(`Error checking out ref '${ref}':`, error.message);
|
|
960
|
-
} else {
|
|
961
|
-
consola.error(`Unknown error occurred while checking out ref '${ref}'.`);
|
|
962
|
-
}
|
|
963
|
-
consola.warn(`Falling back to the default branch.`);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// src/cli/lib/has-github-ssh.ts
|
|
968
|
-
import { execSync as execSync2 } from "child_process";
|
|
969
|
-
function hasGitHubSSH() {
|
|
970
|
-
try {
|
|
971
|
-
const output = execSync2("ssh -T git@github.com", {
|
|
972
|
-
encoding: "utf8",
|
|
973
|
-
stdio: "pipe"
|
|
974
|
-
}).toString();
|
|
975
|
-
return output.includes("successfully authenticated");
|
|
976
|
-
} catch (error) {
|
|
977
|
-
if (isExecException(error)) {
|
|
978
|
-
const stdout = error.stdout ? error.stdout.toString() : "";
|
|
979
|
-
const stderr = error.stderr ? error.stderr.toString() : "";
|
|
980
|
-
const combinedOutput = stdout + stderr;
|
|
981
|
-
if (combinedOutput.includes("successfully authenticated")) {
|
|
982
|
-
return true;
|
|
983
|
-
}
|
|
1079
|
+
async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken, apiHost) {
|
|
1080
|
+
const response = await fetch(
|
|
1081
|
+
`https://${apiHost}/stores/${storeHash}/v3/channels/${channelId}/site`,
|
|
1082
|
+
{
|
|
1083
|
+
method: "PUT",
|
|
1084
|
+
headers: {
|
|
1085
|
+
"X-Auth-Token": accessToken,
|
|
1086
|
+
"Content-Type": "application/json",
|
|
1087
|
+
Accept: "application/json",
|
|
1088
|
+
"X-Correlation-Id": getTelemetry().correlationId
|
|
1089
|
+
},
|
|
1090
|
+
body: JSON.stringify({ url: siteUrl })
|
|
984
1091
|
}
|
|
985
|
-
|
|
1092
|
+
);
|
|
1093
|
+
if (response.status === 401 || response.status === 403) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`Failed to update channel site (${response.status}). Re-run \`catalyst auth login\` to refresh your access token with the store_channel_settings scope.`
|
|
1096
|
+
);
|
|
986
1097
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
// src/cli/lib/reset-branch-to-ref.ts
|
|
990
|
-
import { sync as spawnSync2 } from "cross-spawn";
|
|
991
|
-
function resetBranchToRef(projectDir, ghRef) {
|
|
992
|
-
const spawn = spawnSync2("git", ["reset", "--hard", ghRef, "--"], {
|
|
993
|
-
cwd: projectDir,
|
|
994
|
-
encoding: "utf8",
|
|
995
|
-
shell: false
|
|
996
|
-
});
|
|
997
|
-
const stderr = spawn.stderr.trim();
|
|
998
|
-
if (spawn.status !== 0 && stderr) {
|
|
999
|
-
throw new Error(stderr);
|
|
1098
|
+
if (!response.ok) {
|
|
1099
|
+
throw new Error(`Failed to update channel site: ${response.status} ${response.statusText}`);
|
|
1000
1100
|
}
|
|
1101
|
+
const res = await response.json();
|
|
1102
|
+
const { data } = channelSiteSchema.parse(res);
|
|
1103
|
+
return { id: data.id, url: data.url, channelId: data.channel_id };
|
|
1001
1104
|
}
|
|
1002
1105
|
|
|
1003
|
-
// src/cli/lib/clone-catalyst.ts
|
|
1004
|
-
var cloneCatalyst = ({
|
|
1005
|
-
repository,
|
|
1006
|
-
projectName,
|
|
1007
|
-
projectDir,
|
|
1008
|
-
ghRef,
|
|
1009
|
-
resetMain = false
|
|
1010
|
-
}) => {
|
|
1011
|
-
const useSSH = hasGitHubSSH();
|
|
1012
|
-
consola.info(`Cloning ${repository} using ${useSSH ? "SSH" : "HTTPS"}...`);
|
|
1013
|
-
const cloneCommand = `git clone ${useSSH ? `git@github.com:${repository}` : `https://github.com/${repository}`}.git${projectName ? ` ${projectName}` : ""}`;
|
|
1014
|
-
execSync3(cloneCommand, { stdio: "inherit" });
|
|
1015
|
-
execSync3("git remote rename origin upstream", { cwd: projectDir, stdio: "inherit" });
|
|
1016
|
-
if (ghRef) {
|
|
1017
|
-
if (resetMain) {
|
|
1018
|
-
execSync3("git checkout -b main", { cwd: projectDir, stdio: "inherit" });
|
|
1019
|
-
resetBranchToRef(projectDir, ghRef);
|
|
1020
|
-
consola.success(`Reset main to ${ghRef} successfully.`);
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
checkoutRef(projectDir, ghRef);
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
1106
|
// src/cli/lib/commerce-hosting.ts
|
|
1028
|
-
import { input, select, Separator } from "@inquirer/prompts";
|
|
1107
|
+
import { confirm as confirm2, input as input2, select, Separator } from "@inquirer/prompts";
|
|
1029
1108
|
import { colorize as colorize2 } from "consola/utils";
|
|
1030
1109
|
import {
|
|
1031
|
-
existsSync as
|
|
1110
|
+
existsSync as existsSync2,
|
|
1032
1111
|
lstatSync,
|
|
1033
1112
|
mkdirSync,
|
|
1034
1113
|
readFileSync as readFileSync2,
|
|
@@ -1036,8 +1115,8 @@ import {
|
|
|
1036
1115
|
unlinkSync,
|
|
1037
1116
|
writeFileSync
|
|
1038
1117
|
} from "fs";
|
|
1039
|
-
import { dirname as dirname2, join as
|
|
1040
|
-
import { z as
|
|
1118
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1119
|
+
import { z as z7 } from "zod";
|
|
1041
1120
|
|
|
1042
1121
|
// src/cli/lib/sort-package-json.ts
|
|
1043
1122
|
var FIELD_ORDER = [
|
|
@@ -1067,8 +1146,8 @@ function sortPackageJsonFields(pkg) {
|
|
|
1067
1146
|
|
|
1068
1147
|
// src/cli/lib/commerce-hosting.ts
|
|
1069
1148
|
var OPENNEXT_CLOUDFLARE_VERSION = "1.17.3";
|
|
1070
|
-
var corePackageJsonSchema =
|
|
1071
|
-
dependencies:
|
|
1149
|
+
var corePackageJsonSchema = z7.looseObject({
|
|
1150
|
+
dependencies: z7.record(z7.string(), z7.string()).optional()
|
|
1072
1151
|
});
|
|
1073
1152
|
var writeJson = (path, value) => {
|
|
1074
1153
|
mkdirSync(dirname2(path), { recursive: true });
|
|
@@ -1076,7 +1155,7 @@ var writeJson = (path, value) => {
|
|
|
1076
1155
|
`);
|
|
1077
1156
|
};
|
|
1078
1157
|
var symlinkRootEnvToCore = (projectDir) => {
|
|
1079
|
-
const coreEnvPath =
|
|
1158
|
+
const coreEnvPath = join5(projectDir, "core", ".env.local");
|
|
1080
1159
|
if (lstatSync(coreEnvPath, { throwIfNoEntry: false })) return;
|
|
1081
1160
|
try {
|
|
1082
1161
|
symlinkSync("../.env.local", coreEnvPath);
|
|
@@ -1090,20 +1169,53 @@ You will need to keep .env.local and core/.env.local in sync manually.`
|
|
|
1090
1169
|
}
|
|
1091
1170
|
};
|
|
1092
1171
|
var convertProxyToMiddleware = (projectDir) => {
|
|
1093
|
-
const proxyPath =
|
|
1094
|
-
const middlewarePath =
|
|
1095
|
-
if (!
|
|
1172
|
+
const proxyPath = join5(projectDir, "core", "proxy.ts");
|
|
1173
|
+
const middlewarePath = join5(projectDir, "core", "middleware.ts");
|
|
1174
|
+
if (!existsSync2(proxyPath)) return;
|
|
1096
1175
|
const contents = readFileSync2(proxyPath, "utf-8").replace("export const proxy", "export const middleware").replace("export const config = {", "export const config = {\n runtime: 'experimental-edge',");
|
|
1097
1176
|
writeFileSync(middlewarePath, contents);
|
|
1098
1177
|
unlinkSync(proxyPath);
|
|
1099
1178
|
};
|
|
1100
|
-
var
|
|
1179
|
+
var cleanupCloudflareIncompatibilities = async (projectDir) => {
|
|
1180
|
+
const instrumentationPath = join5(projectDir, "core", "instrumentation.ts");
|
|
1181
|
+
if (!existsSync2(instrumentationPath)) return;
|
|
1182
|
+
if (!process.stdin.isTTY) {
|
|
1183
|
+
consola.warn(
|
|
1184
|
+
"core/instrumentation.ts is present and may be incompatible with the Cloudflare Workers bundle (the default @vercel/otel scaffolding throws at cold start under workerd). Skipping automatic cleanup in non-interactive mode \u2014 re-run interactively to remove it, or delete/gate it manually."
|
|
1185
|
+
);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const shouldRemove = await confirm2({
|
|
1189
|
+
message: 'Catalyst found core/instrumentation.ts, which is incompatible with the Cloudflare Workers bundle when it uses @vercel/otel (causes "Failed to prepare server" at cold start). Remove it and drop @vercel/otel from core/package.json?',
|
|
1190
|
+
default: true
|
|
1191
|
+
});
|
|
1192
|
+
if (!shouldRemove) {
|
|
1193
|
+
consola.info(
|
|
1194
|
+
'Leaving core/instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
|
|
1195
|
+
);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
unlinkSync(instrumentationPath);
|
|
1199
|
+
consola.info("Removed core/instrumentation.ts (incompatible with Cloudflare Workers).");
|
|
1200
|
+
const corePackageJsonPath = join5(projectDir, "core", "package.json");
|
|
1201
|
+
if (existsSync2(corePackageJsonPath)) {
|
|
1202
|
+
const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
|
|
1203
|
+
if (pkg.dependencies && "@vercel/otel" in pkg.dependencies) {
|
|
1204
|
+
const { "@vercel/otel": _removedVercelOtel, ...preservedDeps } = pkg.dependencies;
|
|
1205
|
+
pkg.dependencies = preservedDeps;
|
|
1206
|
+
writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
|
|
1207
|
+
consola.info("Dropped @vercel/otel from core/package.json.");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
var setupCommerceHosting = async ({
|
|
1101
1212
|
projectDir,
|
|
1102
1213
|
projectUuid,
|
|
1103
1214
|
storeHash,
|
|
1104
1215
|
accessToken
|
|
1105
1216
|
}) => {
|
|
1106
|
-
|
|
1217
|
+
await cleanupCloudflareIncompatibilities(projectDir);
|
|
1218
|
+
const corePackageJsonPath = join5(projectDir, "core", "package.json");
|
|
1107
1219
|
const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
|
|
1108
1220
|
pkg.dependencies = {
|
|
1109
1221
|
...pkg.dependencies,
|
|
@@ -1116,7 +1228,7 @@ var setupCommerceHosting = ({
|
|
|
1116
1228
|
};
|
|
1117
1229
|
if (storeHash) projectJson.storeHash = storeHash;
|
|
1118
1230
|
if (accessToken) projectJson.accessToken = accessToken;
|
|
1119
|
-
writeJson(
|
|
1231
|
+
writeJson(join5(projectDir, "core", ".bigcommerce", "project.json"), projectJson);
|
|
1120
1232
|
symlinkRootEnvToCore(projectDir);
|
|
1121
1233
|
convertProxyToMiddleware(projectDir);
|
|
1122
1234
|
};
|
|
@@ -1127,48 +1239,41 @@ var NoLinkedProjectError = class extends Error {
|
|
|
1127
1239
|
}
|
|
1128
1240
|
};
|
|
1129
1241
|
async function promptForNewProjectName(api) {
|
|
1130
|
-
const newProjectName = await
|
|
1131
|
-
|
|
1132
|
-
});
|
|
1133
|
-
const data = await createProject(
|
|
1134
|
-
String(newProjectName),
|
|
1135
|
-
api.storeHash,
|
|
1136
|
-
api.accessToken,
|
|
1137
|
-
api.apiHost
|
|
1138
|
-
);
|
|
1242
|
+
const newProjectName = await input2({ message: "Enter a name for the new project:" });
|
|
1243
|
+
const data = await createProject(newProjectName, api.storeHash, api.accessToken, api.apiHost);
|
|
1139
1244
|
consola.success(`Project "${data.name}" created successfully.`);
|
|
1140
|
-
return { uuid: data.uuid, name: data.name };
|
|
1245
|
+
return { uuid: data.uuid, name: data.name, deployment_hostnames: [] };
|
|
1141
1246
|
}
|
|
1142
1247
|
async function selectOrCreateInfrastructureProject(api, linkedProjectUuid) {
|
|
1143
1248
|
consola.start("Fetching projects...");
|
|
1144
1249
|
const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
|
|
1145
1250
|
consola.success("Projects fetched.");
|
|
1146
1251
|
if (existingProjects.length === 0) {
|
|
1147
|
-
const shouldCreate = await
|
|
1148
|
-
"There are not any hosting projects that you can link to yet. Would you like to create one?",
|
|
1149
|
-
|
|
1150
|
-
);
|
|
1252
|
+
const shouldCreate = await confirm2({
|
|
1253
|
+
message: "There are not any hosting projects that you can link to yet. Would you like to create one?",
|
|
1254
|
+
default: true
|
|
1255
|
+
});
|
|
1151
1256
|
if (!shouldCreate) {
|
|
1152
1257
|
throw new NoLinkedProjectError();
|
|
1153
1258
|
}
|
|
1154
1259
|
return promptForNewProjectName(api);
|
|
1155
1260
|
}
|
|
1156
|
-
const
|
|
1261
|
+
const choices = [
|
|
1157
1262
|
...existingProjects.map((p) => ({
|
|
1158
|
-
|
|
1263
|
+
name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize2("green", "[linked]")}` : p.name,
|
|
1159
1264
|
value: p.uuid,
|
|
1160
|
-
|
|
1265
|
+
description: p.uuid
|
|
1161
1266
|
})),
|
|
1162
1267
|
{
|
|
1163
|
-
|
|
1268
|
+
name: "Create a new project",
|
|
1164
1269
|
value: "create",
|
|
1165
|
-
|
|
1270
|
+
description: "Create a new hosting project for this Catalyst storefront."
|
|
1166
1271
|
}
|
|
1167
1272
|
];
|
|
1168
|
-
const selected = await
|
|
1169
|
-
"Select a project or create a new project (Press <enter> to select).",
|
|
1170
|
-
|
|
1171
|
-
);
|
|
1273
|
+
const selected = await select({
|
|
1274
|
+
message: "Select a project or create a new project (Press <enter> to select).",
|
|
1275
|
+
choices
|
|
1276
|
+
});
|
|
1172
1277
|
if (selected === "create") {
|
|
1173
1278
|
return promptForNewProjectName(api);
|
|
1174
1279
|
}
|
|
@@ -1238,7 +1343,7 @@ async function promptForCommerceHostingProject(api, defaultName, autoUseDefaultN
|
|
|
1238
1343
|
return selected;
|
|
1239
1344
|
}
|
|
1240
1345
|
async function promptAndCreateCommerceHostingProject(api, takenNames, defaultName) {
|
|
1241
|
-
const projectName = await
|
|
1346
|
+
const projectName = await input2({
|
|
1242
1347
|
message: "What would you like to name your Commerce Hosting project?",
|
|
1243
1348
|
default: defaultName,
|
|
1244
1349
|
validate: (value) => {
|
|
@@ -1267,7 +1372,7 @@ async function promptAndCreateCommerceHostingProject(api, takenNames, defaultNam
|
|
|
1267
1372
|
api.apiHost
|
|
1268
1373
|
);
|
|
1269
1374
|
consola.success(`Commerce Hosting project "${created.name}" created successfully`);
|
|
1270
|
-
return { uuid: created.uuid, name: created.name };
|
|
1375
|
+
return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
|
|
1271
1376
|
} catch (error) {
|
|
1272
1377
|
if (error instanceof InfrastructureProjectValidationError) {
|
|
1273
1378
|
consola.error(error.message);
|
|
@@ -1305,7 +1410,7 @@ async function autoCreateCommerceHostingProject(api, name, existingProjects, use
|
|
|
1305
1410
|
try {
|
|
1306
1411
|
const created = await createProject(name, api.storeHash, api.accessToken, api.apiHost);
|
|
1307
1412
|
consola.success(`Commerce Hosting project "${created.name}" created successfully`);
|
|
1308
|
-
return { uuid: created.uuid, name: created.name };
|
|
1413
|
+
return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
|
|
1309
1414
|
} catch (error) {
|
|
1310
1415
|
if (error instanceof InfrastructureProjectValidationError) {
|
|
1311
1416
|
consola.error(
|
|
@@ -1314,10 +1419,424 @@ Re-run with a different --project-name.`
|
|
|
1314
1419
|
);
|
|
1315
1420
|
process.exit(1);
|
|
1316
1421
|
}
|
|
1317
|
-
throw error;
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/cli/lib/channel-site-flow.ts
|
|
1427
|
+
async function resolveProject(options) {
|
|
1428
|
+
const api = {
|
|
1429
|
+
storeHash: options.storeHash,
|
|
1430
|
+
accessToken: options.accessToken,
|
|
1431
|
+
apiHost: options.apiHost
|
|
1432
|
+
};
|
|
1433
|
+
if (options.projectUuid) {
|
|
1434
|
+
const projects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
|
|
1435
|
+
const matched = projects.find((p) => p.uuid === options.projectUuid);
|
|
1436
|
+
if (matched) return matched;
|
|
1437
|
+
consola.warn(
|
|
1438
|
+
`Project ${options.projectUuid} not found on this store. Pick another to continue.`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
return selectOrCreateInfrastructureProject(api, options.projectUuid);
|
|
1442
|
+
}
|
|
1443
|
+
async function resolveChannel(options) {
|
|
1444
|
+
if (options.channelId !== void 0) {
|
|
1445
|
+
return { id: options.channelId };
|
|
1446
|
+
}
|
|
1447
|
+
consola.start("Fetching channels...");
|
|
1448
|
+
const channels = await fetchAvailableChannels(
|
|
1449
|
+
options.storeHash,
|
|
1450
|
+
options.accessToken,
|
|
1451
|
+
options.apiHost
|
|
1452
|
+
);
|
|
1453
|
+
consola.success("Channels fetched.");
|
|
1454
|
+
const catalystChannels = channels.filter((c) => c.platform === "catalyst");
|
|
1455
|
+
if (catalystChannels.length === 0) {
|
|
1456
|
+
throw new Error(
|
|
1457
|
+
"No Catalyst channels found on this store. Create one with `catalyst create` and try again."
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const id = await select2({
|
|
1461
|
+
message: "Select the channel to update.",
|
|
1462
|
+
choices: catalystChannels.map((c) => ({
|
|
1463
|
+
name: c.name,
|
|
1464
|
+
value: c.id,
|
|
1465
|
+
description: `id: ${c.id}`
|
|
1466
|
+
}))
|
|
1467
|
+
});
|
|
1468
|
+
const matched = catalystChannels.find((c) => c.id === id);
|
|
1469
|
+
return { id, name: matched?.name };
|
|
1470
|
+
}
|
|
1471
|
+
async function resolveHostname(project2, options) {
|
|
1472
|
+
if (options.hostname) {
|
|
1473
|
+
return options.hostname;
|
|
1474
|
+
}
|
|
1475
|
+
if (project2.deployment_hostnames.length === 0) {
|
|
1476
|
+
throw new Error(
|
|
1477
|
+
`Project "${project2.name}" has no deployment hostnames yet. Run \`catalyst deploy\` first to create one.`
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
const ordered = options.preferHostname ? [
|
|
1481
|
+
...project2.deployment_hostnames.filter((h) => h === options.preferHostname),
|
|
1482
|
+
...project2.deployment_hostnames.filter((h) => h !== options.preferHostname)
|
|
1483
|
+
] : project2.deployment_hostnames;
|
|
1484
|
+
const selected = await select2({
|
|
1485
|
+
message: "Select the hostname to point the channel at.",
|
|
1486
|
+
choices: ordered.map((h) => ({ name: h, value: h }))
|
|
1487
|
+
});
|
|
1488
|
+
return selected;
|
|
1489
|
+
}
|
|
1490
|
+
async function runChannelSiteUrlFlow(options) {
|
|
1491
|
+
const project2 = await resolveProject(options);
|
|
1492
|
+
const channel2 = await resolveChannel(options);
|
|
1493
|
+
const hostname = await resolveHostname(project2, options);
|
|
1494
|
+
const siteUrl = hostname.startsWith("https://") ? hostname : `https://${hostname}`;
|
|
1495
|
+
await updateChannelSiteUrl(
|
|
1496
|
+
channel2.id,
|
|
1497
|
+
siteUrl,
|
|
1498
|
+
options.storeHash,
|
|
1499
|
+
options.accessToken,
|
|
1500
|
+
options.apiHost
|
|
1501
|
+
);
|
|
1502
|
+
const channelLabel = channel2.name ? `"${channel2.name}" (${channel2.id})` : String(channel2.id);
|
|
1503
|
+
consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/cli/lib/env-config.ts
|
|
1507
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1508
|
+
var parseEnvAssignment = (input5) => {
|
|
1509
|
+
const separatorIndex = input5.indexOf("=");
|
|
1510
|
+
if (separatorIndex === -1) {
|
|
1511
|
+
throw new Error(`Invalid env var format: ${input5}. Expected format: KEY=VALUE`);
|
|
1512
|
+
}
|
|
1513
|
+
const key = input5.slice(0, separatorIndex).trim();
|
|
1514
|
+
const value = input5.slice(separatorIndex + 1).trim();
|
|
1515
|
+
if (!key || !value) {
|
|
1516
|
+
throw new Error(`Invalid env var format: ${input5}. Expected format: KEY=VALUE`);
|
|
1517
|
+
}
|
|
1518
|
+
if (!ENV_KEY_PATTERN.test(key)) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`Invalid env var name: ${key}. Names must start with a letter or underscore and contain only letters, numbers, and underscores.`
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
return { key, value };
|
|
1524
|
+
};
|
|
1525
|
+
var getStoredEnv = (config2) => {
|
|
1526
|
+
return config2.get("env") ?? {};
|
|
1527
|
+
};
|
|
1528
|
+
var toDeploymentSecrets = (envVars) => {
|
|
1529
|
+
return Object.entries(envVars).map(([key, value]) => ({ type: "secret", key, value }));
|
|
1530
|
+
};
|
|
1531
|
+
var mergeDeploymentSecrets = (persisted, flagSecrets) => {
|
|
1532
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1533
|
+
persisted.forEach((secret) => byKey.set(secret.key, secret));
|
|
1534
|
+
flagSecrets.forEach((secret) => byKey.set(secret.key, secret));
|
|
1535
|
+
return Array.from(byKey.values());
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
// src/cli/lib/resolve-credentials.ts
|
|
1539
|
+
function resolveCredentials(options, config2) {
|
|
1540
|
+
const storeHash = options.storeHash ?? config2.get("storeHash");
|
|
1541
|
+
const accessToken = options.accessToken ?? config2.get("accessToken");
|
|
1542
|
+
if (!storeHash || !accessToken) {
|
|
1543
|
+
consola.error("Missing credentials.");
|
|
1544
|
+
consola.info(
|
|
1545
|
+
"Run `catalyst auth login`, or provide --store-hash and --access-token flags (or set CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables)."
|
|
1546
|
+
);
|
|
1547
|
+
process.exit(1);
|
|
1548
|
+
throw new Error("Missing credentials");
|
|
1549
|
+
}
|
|
1550
|
+
return { storeHash, accessToken };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/cli/commands/channel.ts
|
|
1554
|
+
var parseChannelId = (value) => {
|
|
1555
|
+
const parsed = Number.parseInt(value, 10);
|
|
1556
|
+
if (Number.isNaN(parsed)) {
|
|
1557
|
+
throw new InvalidArgumentError(`"${value}" is not a valid channel ID (expected a number).`);
|
|
1558
|
+
}
|
|
1559
|
+
return parsed;
|
|
1560
|
+
};
|
|
1561
|
+
async function resolveCredentialsWithLogin(options, config2) {
|
|
1562
|
+
const storeHash = options.storeHash ?? config2.get("storeHash");
|
|
1563
|
+
const accessToken = options.accessToken ?? config2.get("accessToken");
|
|
1564
|
+
if (storeHash && accessToken) {
|
|
1565
|
+
return { storeHash, accessToken };
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
const credentials = await login(options.loginUrl, options.apiHost);
|
|
1569
|
+
config2.set("storeHash", credentials.storeHash);
|
|
1570
|
+
config2.set("accessToken", credentials.accessToken);
|
|
1571
|
+
return credentials;
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
if (error instanceof LoginAbortedError) {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
throw error;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
var update = new Command3("update").configureHelp({ showGlobalOptions: true }).description(
|
|
1580
|
+
"Update a BigCommerce channel's site URL to point at one of your project's deployment hostnames."
|
|
1581
|
+
).addHelpText(
|
|
1582
|
+
"after",
|
|
1583
|
+
`
|
|
1584
|
+
Examples:
|
|
1585
|
+
# Pick a channel and hostname interactively
|
|
1586
|
+
$ catalyst channel update
|
|
1587
|
+
|
|
1588
|
+
# Skip both prompts
|
|
1589
|
+
$ catalyst channel update --channel-id 123 --hostname my-storefront.example.com`
|
|
1590
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
|
|
1591
|
+
new Option4(
|
|
1592
|
+
"--channel-id <id>",
|
|
1593
|
+
"Skip the channel prompt and target this channel directly."
|
|
1594
|
+
).argParser((value) => Number(value))
|
|
1595
|
+
).addOption(
|
|
1596
|
+
new Option4(
|
|
1597
|
+
"--hostname <hostname>",
|
|
1598
|
+
"Skip the hostname prompt and use this hostname directly. Must be one of the project's deployment_hostnames."
|
|
1599
|
+
)
|
|
1600
|
+
).action(async (options) => {
|
|
1601
|
+
const config2 = getProjectConfig();
|
|
1602
|
+
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
1603
|
+
await getTelemetry().identify(storeHash);
|
|
1604
|
+
try {
|
|
1605
|
+
await runChannelSiteUrlFlow({
|
|
1606
|
+
storeHash,
|
|
1607
|
+
accessToken,
|
|
1608
|
+
apiHost: options.apiHost,
|
|
1609
|
+
projectUuid: options.projectUuid ?? config2.get("projectUuid"),
|
|
1610
|
+
channelId: options.channelId,
|
|
1611
|
+
hostname: options.hostname
|
|
1612
|
+
});
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
if (error instanceof NoLinkedProjectError) {
|
|
1615
|
+
consola.info(
|
|
1616
|
+
"When you're ready to create a project, run `catalyst project create` or re-run `catalyst channel update`."
|
|
1617
|
+
);
|
|
1618
|
+
process.exit(0);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
throw error;
|
|
1622
|
+
}
|
|
1623
|
+
process.exit(0);
|
|
1624
|
+
});
|
|
1625
|
+
var link = new Command3("link").configureHelp({ showGlobalOptions: true }).description(
|
|
1626
|
+
"Link this Catalyst project to a BigCommerce channel and write its credentials to .env.local."
|
|
1627
|
+
).addHelpText(
|
|
1628
|
+
"after",
|
|
1629
|
+
`
|
|
1630
|
+
Examples:
|
|
1631
|
+
# Pick a channel interactively (logs you in if needed)
|
|
1632
|
+
$ catalyst channel link
|
|
1633
|
+
|
|
1634
|
+
# Non-interactive
|
|
1635
|
+
$ catalyst channel link --store-hash <hash> --access-token <token> --channel-id 123
|
|
1636
|
+
|
|
1637
|
+
# Append extra environment variables to .env.local
|
|
1638
|
+
$ catalyst channel link --channel-id 123 --env MY_FLAG=1`
|
|
1639
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).addOption(
|
|
1640
|
+
new Option4("--channel-id <id>", "Link this channel directly, skipping the picker.").argParser(
|
|
1641
|
+
parseChannelId
|
|
1642
|
+
)
|
|
1643
|
+
).option(
|
|
1644
|
+
"--env <vars...>",
|
|
1645
|
+
"Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable)."
|
|
1646
|
+
).addOption(
|
|
1647
|
+
new Option4("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
|
|
1648
|
+
).action(async (options) => {
|
|
1649
|
+
const config2 = getProjectConfig();
|
|
1650
|
+
const credentials = await resolveCredentialsWithLogin(options, config2);
|
|
1651
|
+
if (!credentials) {
|
|
1652
|
+
consola.info(
|
|
1653
|
+
"Login aborted. Re-run `catalyst channel link` when you have your credentials ready."
|
|
1654
|
+
);
|
|
1655
|
+
process.exit(0);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const { storeHash, accessToken } = credentials;
|
|
1659
|
+
await getTelemetry().identify(storeHash);
|
|
1660
|
+
let channelId = options.channelId;
|
|
1661
|
+
let channelName;
|
|
1662
|
+
if (channelId === void 0) {
|
|
1663
|
+
const channels = await fetchAvailableChannels(storeHash, accessToken, options.apiHost);
|
|
1664
|
+
if (channels.length === 0) {
|
|
1665
|
+
consola.info(
|
|
1666
|
+
"No storefront channels found on this store. Create one with `catalyst create` and try again."
|
|
1667
|
+
);
|
|
1668
|
+
process.exit(0);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
channelId = await select3({
|
|
1672
|
+
message: "Which channel would you like to link?",
|
|
1673
|
+
choices: sortChannelsByPlatform(channels).map((c) => ({
|
|
1674
|
+
name: c.name,
|
|
1675
|
+
value: c.id,
|
|
1676
|
+
description: channelPlatformLabel(c.platform)
|
|
1677
|
+
}))
|
|
1678
|
+
});
|
|
1679
|
+
channelName = channels.find((c) => c.id === channelId)?.name;
|
|
1680
|
+
}
|
|
1681
|
+
const initData = await getChannelInit(channelId, storeHash, accessToken, options.cliApiOrigin);
|
|
1682
|
+
const envVars = { ...initData.envVars };
|
|
1683
|
+
if (options.env) {
|
|
1684
|
+
options.env.forEach((entry) => {
|
|
1685
|
+
const { key, value } = parseEnvAssignment(entry);
|
|
1686
|
+
envVars[key] = value;
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
outputFileSync(
|
|
1690
|
+
join6(process.cwd(), ".env.local"),
|
|
1691
|
+
`${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
1692
|
+
`
|
|
1693
|
+
);
|
|
1694
|
+
const label = channelName ? `"${channelName}" (${channelId})` : `${channelId}`;
|
|
1695
|
+
consola.success(
|
|
1696
|
+
`Linked to channel ${label} and wrote ${colorize3("cyanBright", ".env.local")}.`
|
|
1697
|
+
);
|
|
1698
|
+
consola.log(`Next steps:
|
|
1699
|
+
|
|
1700
|
+
${colorize3("yellow", "pnpm run dev")}`);
|
|
1701
|
+
process.exit(0);
|
|
1702
|
+
});
|
|
1703
|
+
var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(link).addCommand(update);
|
|
1704
|
+
|
|
1705
|
+
// src/cli/commands/create.ts
|
|
1706
|
+
import { Command as Command4, InvalidArgumentError as InvalidArgumentError2, Option as Option5 } from "@commander-js/extra-typings";
|
|
1707
|
+
import { checkbox, input as input3, select as select4 } from "@inquirer/prompts";
|
|
1708
|
+
import { execSync as execSync4 } from "child_process";
|
|
1709
|
+
import { colorize as colorize4 } from "consola/utils";
|
|
1710
|
+
import { pathExistsSync } from "fs-extra/esm";
|
|
1711
|
+
import kebabCase from "lodash.kebabcase";
|
|
1712
|
+
import { join as join10 } from "path";
|
|
1713
|
+
|
|
1714
|
+
// src/cli/lib/build-workspace-packages.ts
|
|
1715
|
+
import { execSync } from "child_process";
|
|
1716
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1717
|
+
import { join as join7 } from "path";
|
|
1718
|
+
import yoctoSpinner2 from "yocto-spinner";
|
|
1719
|
+
var WORKSPACE_PACKAGES = ["packages/catalyst", "packages/client"];
|
|
1720
|
+
var buildWorkspacePackages = (projectDir) => {
|
|
1721
|
+
const hasCore = existsSync3(join7(projectDir, "core"));
|
|
1722
|
+
const hasAllWorkspacePackages = WORKSPACE_PACKAGES.every(
|
|
1723
|
+
(pkg) => existsSync3(join7(projectDir, pkg))
|
|
1724
|
+
);
|
|
1725
|
+
if (!hasCore || !hasAllWorkspacePackages) return;
|
|
1726
|
+
WORKSPACE_PACKAGES.forEach((pkg) => {
|
|
1727
|
+
const spinner = yoctoSpinner2().start(`Building ${pkg}...`);
|
|
1728
|
+
try {
|
|
1729
|
+
execSync("pnpm build", { cwd: join7(projectDir, pkg), stdio: "ignore" });
|
|
1730
|
+
spinner.success(`Built ${pkg}.`);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1733
|
+
spinner.error(`Failed to build ${pkg}: ${message}`);
|
|
1734
|
+
throw error;
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
// src/cli/lib/clone-catalyst.ts
|
|
1740
|
+
import { execSync as execSync3 } from "child_process";
|
|
1741
|
+
|
|
1742
|
+
// src/cli/lib/checkout-ref.ts
|
|
1743
|
+
import { sync as spawnSync } from "cross-spawn";
|
|
1744
|
+
|
|
1745
|
+
// src/cli/lib/is-exec-exception.ts
|
|
1746
|
+
function isExecException(error) {
|
|
1747
|
+
return typeof error === "object" && error !== null && "stdout" in error && "stderr" in error;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/cli/lib/checkout-ref.ts
|
|
1751
|
+
function checkoutRef(repoDir, ref) {
|
|
1752
|
+
try {
|
|
1753
|
+
const spawn = spawnSync("git", ["checkout", ref, "--"], {
|
|
1754
|
+
cwd: repoDir,
|
|
1755
|
+
encoding: "utf8",
|
|
1756
|
+
shell: false
|
|
1757
|
+
});
|
|
1758
|
+
const stderr = spawn.stderr.trim();
|
|
1759
|
+
if (spawn.status !== 0 && stderr) {
|
|
1760
|
+
throw new Error(stderr);
|
|
1761
|
+
}
|
|
1762
|
+
consola.success(`Checked out ref ${ref} successfully.`);
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
if (isExecException(error)) {
|
|
1765
|
+
const stderr = error.stderr ? error.stderr.toString() : "";
|
|
1766
|
+
if (stderr.includes(`fatal: reference is not a tree: ${ref}`) || stderr.includes(`fatal: ambiguous argument '${ref}'`) || stderr.includes(`unknown revision or path not in the working tree`)) {
|
|
1767
|
+
consola.error(`Ref '${ref}' not found in the repository.`);
|
|
1768
|
+
} else {
|
|
1769
|
+
consola.error(`Error checking out ref '${ref}':`, stderr.trim());
|
|
1770
|
+
}
|
|
1771
|
+
} else if (error instanceof Error) {
|
|
1772
|
+
consola.error(`Error checking out ref '${ref}':`, error.message);
|
|
1773
|
+
} else {
|
|
1774
|
+
consola.error(`Unknown error occurred while checking out ref '${ref}'.`);
|
|
1775
|
+
}
|
|
1776
|
+
consola.warn(`Falling back to the default branch.`);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/cli/lib/has-github-ssh.ts
|
|
1781
|
+
import { execSync as execSync2 } from "child_process";
|
|
1782
|
+
function hasGitHubSSH() {
|
|
1783
|
+
try {
|
|
1784
|
+
const output = execSync2("ssh -T git@github.com", {
|
|
1785
|
+
encoding: "utf8",
|
|
1786
|
+
stdio: "pipe"
|
|
1787
|
+
}).toString();
|
|
1788
|
+
return output.includes("successfully authenticated");
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
if (isExecException(error)) {
|
|
1791
|
+
const stdout = error.stdout ? error.stdout.toString() : "";
|
|
1792
|
+
const stderr = error.stderr ? error.stderr.toString() : "";
|
|
1793
|
+
const combinedOutput = stdout + stderr;
|
|
1794
|
+
if (combinedOutput.includes("successfully authenticated")) {
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// src/cli/lib/reset-branch-to-ref.ts
|
|
1803
|
+
import { sync as spawnSync2 } from "cross-spawn";
|
|
1804
|
+
function resetBranchToRef(projectDir, ghRef) {
|
|
1805
|
+
const spawn = spawnSync2("git", ["reset", "--hard", ghRef, "--"], {
|
|
1806
|
+
cwd: projectDir,
|
|
1807
|
+
encoding: "utf8",
|
|
1808
|
+
shell: false
|
|
1809
|
+
});
|
|
1810
|
+
const stderr = spawn.stderr.trim();
|
|
1811
|
+
if (spawn.status !== 0 && stderr) {
|
|
1812
|
+
throw new Error(stderr);
|
|
1318
1813
|
}
|
|
1319
1814
|
}
|
|
1320
1815
|
|
|
1816
|
+
// src/cli/lib/clone-catalyst.ts
|
|
1817
|
+
var cloneCatalyst = ({
|
|
1818
|
+
repository,
|
|
1819
|
+
projectName,
|
|
1820
|
+
projectDir,
|
|
1821
|
+
ghRef,
|
|
1822
|
+
resetMain = false
|
|
1823
|
+
}) => {
|
|
1824
|
+
const useSSH = hasGitHubSSH();
|
|
1825
|
+
consola.info(`Cloning ${repository} using ${useSSH ? "SSH" : "HTTPS"}...`);
|
|
1826
|
+
const cloneCommand = `git clone ${useSSH ? `git@github.com:${repository}` : `https://github.com/${repository}`}.git${projectName ? ` ${projectName}` : ""}`;
|
|
1827
|
+
execSync3(cloneCommand, { stdio: "inherit" });
|
|
1828
|
+
execSync3("git remote rename origin upstream", { cwd: projectDir, stdio: "inherit" });
|
|
1829
|
+
if (ghRef) {
|
|
1830
|
+
if (resetMain) {
|
|
1831
|
+
execSync3("git checkout -b main", { cwd: projectDir, stdio: "inherit" });
|
|
1832
|
+
resetBranchToRef(projectDir, ghRef);
|
|
1833
|
+
consola.success(`Reset main to ${ghRef} successfully.`);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
checkoutRef(projectDir, ghRef);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1321
1840
|
// src/cli/lib/install-dependencies.ts
|
|
1322
1841
|
import { installDependencies as installDeps } from "nypm";
|
|
1323
1842
|
import yoctoSpinner3 from "yocto-spinner";
|
|
@@ -1334,7 +1853,7 @@ var installDependencies = async (projectDir) => {
|
|
|
1334
1853
|
};
|
|
1335
1854
|
|
|
1336
1855
|
// src/cli/lib/localization.ts
|
|
1337
|
-
import { z as
|
|
1856
|
+
import { z as z8 } from "zod";
|
|
1338
1857
|
var allowedLocales = [
|
|
1339
1858
|
"en",
|
|
1340
1859
|
"da",
|
|
@@ -1356,13 +1875,13 @@ var allowedLocales = [
|
|
|
1356
1875
|
"pt-BR",
|
|
1357
1876
|
"sv"
|
|
1358
1877
|
];
|
|
1359
|
-
var AvailableLocalesSuccessSchema =
|
|
1360
|
-
data:
|
|
1361
|
-
|
|
1362
|
-
id:
|
|
1363
|
-
name:
|
|
1364
|
-
fallback:
|
|
1365
|
-
is_supported:
|
|
1878
|
+
var AvailableLocalesSuccessSchema = z8.object({
|
|
1879
|
+
data: z8.array(
|
|
1880
|
+
z8.object({
|
|
1881
|
+
id: z8.string(),
|
|
1882
|
+
name: z8.string(),
|
|
1883
|
+
fallback: z8.string().nullable(),
|
|
1884
|
+
is_supported: z8.boolean()
|
|
1366
1885
|
})
|
|
1367
1886
|
)
|
|
1368
1887
|
});
|
|
@@ -1378,6 +1897,7 @@ var getAvailableLocales = async (storeHash, accessToken, apiHost) => {
|
|
|
1378
1897
|
}
|
|
1379
1898
|
}
|
|
1380
1899
|
);
|
|
1900
|
+
assertAuthorized(response);
|
|
1381
1901
|
if (!response.ok) {
|
|
1382
1902
|
throw new Error(
|
|
1383
1903
|
`GET /v3/settings/store/available-locales failed: ${response.status} ${response.statusText}`
|
|
@@ -1386,41 +1906,13 @@ var getAvailableLocales = async (storeHash, accessToken, apiHost) => {
|
|
|
1386
1906
|
return AvailableLocalesSuccessSchema.parse(await response.json()).data.filter(({ id }) => allowedLocales.includes(id)).map(({ name, id }) => ({ name: `${name} (${id})`, value: id }));
|
|
1387
1907
|
};
|
|
1388
1908
|
|
|
1389
|
-
// src/cli/lib/login.ts
|
|
1390
|
-
import { colorize as colorize3 } from "consola/utils";
|
|
1391
|
-
import open2 from "open";
|
|
1392
|
-
import yoctoSpinner4 from "yocto-spinner";
|
|
1393
|
-
async function login2(loginUrl) {
|
|
1394
|
-
const deviceCode = await requestDeviceCode(loginUrl);
|
|
1395
|
-
consola.info(
|
|
1396
|
-
`${colorize3("yellow", "Your one-time code:")} ${colorize3("bold", deviceCode.user_code)}`
|
|
1397
|
-
);
|
|
1398
|
-
try {
|
|
1399
|
-
await open2(deviceCode.verification_uri);
|
|
1400
|
-
consola.info(`Opened ${deviceCode.verification_uri} in your browser.`);
|
|
1401
|
-
} catch {
|
|
1402
|
-
consola.info(`Open ${deviceCode.verification_uri} in your browser and enter the code above.`);
|
|
1403
|
-
}
|
|
1404
|
-
const spinner = yoctoSpinner4().start("Waiting for authentication...");
|
|
1405
|
-
const credentials = await waitForDeviceToken(
|
|
1406
|
-
loginUrl,
|
|
1407
|
-
deviceCode.device_code,
|
|
1408
|
-
deviceCode.interval
|
|
1409
|
-
);
|
|
1410
|
-
spinner.success("Authentication complete.");
|
|
1411
|
-
return {
|
|
1412
|
-
storeHash: credentials.store_hash,
|
|
1413
|
-
accessToken: credentials.access_token
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
1909
|
// src/cli/lib/setup-core-project.ts
|
|
1418
1910
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1419
|
-
import { dirname as dirname3, join as
|
|
1420
|
-
import { z as
|
|
1421
|
-
var corePackageJsonSchema2 =
|
|
1422
|
-
scripts:
|
|
1423
|
-
dependencies:
|
|
1911
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1912
|
+
import { z as z9 } from "zod";
|
|
1913
|
+
var corePackageJsonSchema2 = z9.looseObject({
|
|
1914
|
+
scripts: z9.record(z9.string(), z9.string()).optional(),
|
|
1915
|
+
dependencies: z9.record(z9.string(), z9.string()).optional()
|
|
1424
1916
|
});
|
|
1425
1917
|
var writeJson2 = (path, value) => {
|
|
1426
1918
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
@@ -1428,7 +1920,7 @@ var writeJson2 = (path, value) => {
|
|
|
1428
1920
|
`);
|
|
1429
1921
|
};
|
|
1430
1922
|
var setupCoreProject = (projectDir) => {
|
|
1431
|
-
const corePackageJsonPath =
|
|
1923
|
+
const corePackageJsonPath = join8(projectDir, "core", "package.json");
|
|
1432
1924
|
const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync3(corePackageJsonPath, "utf-8")));
|
|
1433
1925
|
pkg.scripts = {
|
|
1434
1926
|
...pkg.scripts,
|
|
@@ -1443,38 +1935,12 @@ var setupCoreProject = (projectDir) => {
|
|
|
1443
1935
|
writeJson2(corePackageJsonPath, sortPackageJsonFields(pkg));
|
|
1444
1936
|
};
|
|
1445
1937
|
|
|
1446
|
-
// src/cli/lib/shared-options.ts
|
|
1447
|
-
import { Option as Option3 } from "commander";
|
|
1448
|
-
var storeHashOption = () => new Option3(
|
|
1449
|
-
"--store-hash <hash>",
|
|
1450
|
-
"BigCommerce store hash. Can be found in the URL of your store Control Panel. Read from .bigcommerce/project.json or .env when not provided."
|
|
1451
|
-
).env("CATALYST_STORE_HASH");
|
|
1452
|
-
var accessTokenOption = () => new Option3(
|
|
1453
|
-
"--access-token <token>",
|
|
1454
|
-
"BigCommerce access token. Can be found after creating a store-level API account. Read from .bigcommerce/project.json or .env when not provided."
|
|
1455
|
-
).env("CATALYST_ACCESS_TOKEN");
|
|
1456
|
-
var apiHostOption = () => new Option3("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com").hideHelp();
|
|
1457
|
-
var projectUuidOption = () => new Option3(
|
|
1458
|
-
"--project-uuid <uuid>",
|
|
1459
|
-
"BigCommerce infrastructure project UUID. Read from .bigcommerce/project.json or .env when not provided."
|
|
1460
|
-
).env("CATALYST_PROJECT_UUID");
|
|
1461
|
-
var resolveProjectUuid = (options) => {
|
|
1462
|
-
const config2 = getProjectConfig();
|
|
1463
|
-
const projectUuid = options.projectUuid ?? config2.get("projectUuid");
|
|
1464
|
-
if (!projectUuid) {
|
|
1465
|
-
throw new Error(
|
|
1466
|
-
"Project UUID is required. Please run either `catalyst project link` or `catalyst project create` or this command again with --project-uuid <uuid>."
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
return projectUuid;
|
|
1470
|
-
};
|
|
1471
|
-
|
|
1472
1938
|
// src/cli/lib/write-env.ts
|
|
1473
|
-
import { outputFileSync } from "fs-extra/esm";
|
|
1474
|
-
import { join as
|
|
1939
|
+
import { outputFileSync as outputFileSync2 } from "fs-extra/esm";
|
|
1940
|
+
import { join as join9 } from "path";
|
|
1475
1941
|
var writeEnv = (projectDir, envVars) => {
|
|
1476
|
-
|
|
1477
|
-
|
|
1942
|
+
outputFileSync2(
|
|
1943
|
+
join9(projectDir, "core", ".env.local"),
|
|
1478
1944
|
`${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
1479
1945
|
`
|
|
1480
1946
|
);
|
|
@@ -1485,31 +1951,31 @@ function getPlatformCheckCommand(command) {
|
|
|
1485
1951
|
const isWindows = process.platform === "win32";
|
|
1486
1952
|
return isWindows ? `where.exe ${command}` : `which ${command}`;
|
|
1487
1953
|
}
|
|
1488
|
-
function
|
|
1954
|
+
function parseChannelId2(value) {
|
|
1489
1955
|
const parsed = parseInt(value, 10);
|
|
1490
1956
|
if (Number.isNaN(parsed)) {
|
|
1491
|
-
throw new
|
|
1957
|
+
throw new InvalidArgumentError2(`"${value}" is not a valid channel ID (expected a number).`);
|
|
1492
1958
|
}
|
|
1493
1959
|
return parsed;
|
|
1494
1960
|
}
|
|
1495
1961
|
function parseEnvFlag(value, previous = {}) {
|
|
1496
1962
|
const eqIdx = value.indexOf("=");
|
|
1497
1963
|
if (eqIdx <= 0) {
|
|
1498
|
-
throw new
|
|
1964
|
+
throw new InvalidArgumentError2(`Expected KEY=VALUE, got "${value}".`);
|
|
1499
1965
|
}
|
|
1500
1966
|
const key = value.substring(0, eqIdx);
|
|
1501
1967
|
const val = value.substring(eqIdx + 1);
|
|
1502
1968
|
if (!val) {
|
|
1503
|
-
throw new
|
|
1969
|
+
throw new InvalidArgumentError2(`Expected KEY=VALUE with non-empty value, got "${value}".`);
|
|
1504
1970
|
}
|
|
1505
1971
|
return { ...previous, [key]: val };
|
|
1506
1972
|
}
|
|
1507
1973
|
async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrigin) {
|
|
1508
|
-
const newChannelName = await
|
|
1974
|
+
const newChannelName = await input3({
|
|
1509
1975
|
message: "What would you like to name your new channel?"
|
|
1510
1976
|
});
|
|
1511
1977
|
const availableLocales = await getAvailableLocales(storeHash, accessToken, apiHost);
|
|
1512
|
-
const storefrontLocale = await
|
|
1978
|
+
const storefrontLocale = await select4({
|
|
1513
1979
|
message: "Which default language would you like to set for your channel?",
|
|
1514
1980
|
default: "en",
|
|
1515
1981
|
choices: availableLocales,
|
|
@@ -1519,7 +1985,7 @@ async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrig
|
|
|
1519
1985
|
}
|
|
1520
1986
|
}
|
|
1521
1987
|
});
|
|
1522
|
-
const shouldAddAdditionalLocales = await
|
|
1988
|
+
const shouldAddAdditionalLocales = await select4({
|
|
1523
1989
|
message: "Would you like to add additional languages?",
|
|
1524
1990
|
choices: [
|
|
1525
1991
|
{ name: "Yes", value: true },
|
|
@@ -1528,21 +1994,14 @@ async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrig
|
|
|
1528
1994
|
});
|
|
1529
1995
|
let additionalLocales = [];
|
|
1530
1996
|
if (shouldAddAdditionalLocales) {
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
if (selected.length > 4) {
|
|
1538
|
-
consola.warn("You can only select up to 4 additional languages. Please try again.");
|
|
1539
|
-
return pickLocales();
|
|
1540
|
-
}
|
|
1541
|
-
return selected;
|
|
1542
|
-
};
|
|
1543
|
-
additionalLocales = await pickLocales();
|
|
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
|
+
});
|
|
1544
2003
|
}
|
|
1545
|
-
const shouldInstallSampleData = await
|
|
2004
|
+
const shouldInstallSampleData = await select4({
|
|
1546
2005
|
message: "Would you like to install sample data?",
|
|
1547
2006
|
choices: [
|
|
1548
2007
|
{ name: "Yes", value: true },
|
|
@@ -1560,23 +2019,13 @@ async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrig
|
|
|
1560
2019
|
);
|
|
1561
2020
|
}
|
|
1562
2021
|
async function handleChannelSelection(storeHash, accessToken, apiHost) {
|
|
1563
|
-
const channelSortOrder = ["catalyst", "next", "bigcommerce"];
|
|
1564
2022
|
const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
|
|
1565
|
-
const existingChannel = await
|
|
2023
|
+
const existingChannel = await select4({
|
|
1566
2024
|
message: "Which channel would you like to use?",
|
|
1567
|
-
choices: channels.
|
|
1568
|
-
const aIndex = channelSortOrder.indexOf(a.platform);
|
|
1569
|
-
const bIndex = channelSortOrder.indexOf(b.platform);
|
|
1570
|
-
if (aIndex === -1 && bIndex === -1) {
|
|
1571
|
-
return 0;
|
|
1572
|
-
}
|
|
1573
|
-
if (aIndex === -1) return 1;
|
|
1574
|
-
if (bIndex === -1) return -1;
|
|
1575
|
-
return aIndex - bIndex;
|
|
1576
|
-
}).map((ch) => ({
|
|
2025
|
+
choices: sortChannelsByPlatform(channels).map((ch) => ({
|
|
1577
2026
|
name: ch.name,
|
|
1578
2027
|
value: ch,
|
|
1579
|
-
description: `Channel Platform: ${
|
|
2028
|
+
description: `Channel Platform: ${channelPlatformLabel(ch.platform)}`
|
|
1580
2029
|
}))
|
|
1581
2030
|
});
|
|
1582
2031
|
return existingChannel.id;
|
|
@@ -1589,7 +2038,7 @@ async function setupProject(options) {
|
|
|
1589
2038
|
}
|
|
1590
2039
|
if (projectName) {
|
|
1591
2040
|
projectName = kebabCase(projectName);
|
|
1592
|
-
projectDir =
|
|
2041
|
+
projectDir = join10(options.projectDir, projectName);
|
|
1593
2042
|
if (pathExistsSync(projectDir)) {
|
|
1594
2043
|
consola.error(`${projectDir} already exists`);
|
|
1595
2044
|
process.exit(1);
|
|
@@ -1599,13 +2048,13 @@ async function setupProject(options) {
|
|
|
1599
2048
|
const validateProjectName = (i) => {
|
|
1600
2049
|
const formatted = kebabCase(i);
|
|
1601
2050
|
if (!formatted) return "Project name is required";
|
|
1602
|
-
const targetDir =
|
|
2051
|
+
const targetDir = join10(options.projectDir, formatted);
|
|
1603
2052
|
if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`;
|
|
1604
2053
|
projectName = formatted;
|
|
1605
2054
|
projectDir = targetDir;
|
|
1606
2055
|
return true;
|
|
1607
2056
|
};
|
|
1608
|
-
await
|
|
2057
|
+
await input3({
|
|
1609
2058
|
message: "What do you want to name your project directory?",
|
|
1610
2059
|
default: "my-catalyst-app",
|
|
1611
2060
|
validate: validateProjectName
|
|
@@ -1631,7 +2080,7 @@ function checkRequiredTools() {
|
|
|
1631
2080
|
process.exit(1);
|
|
1632
2081
|
}
|
|
1633
2082
|
}
|
|
1634
|
-
var create = new
|
|
2083
|
+
var create = new Command4("create").configureHelp({ showGlobalOptions: true }).description("Scaffold and connect a Catalyst storefront to your BigCommerce store.").addHelpText(
|
|
1635
2084
|
"after",
|
|
1636
2085
|
`
|
|
1637
2086
|
Examples:
|
|
@@ -1643,7 +2092,7 @@ Examples:
|
|
|
1643
2092
|
|
|
1644
2093
|
# Eagerly set up Commerce Hosting at create time
|
|
1645
2094
|
$ catalyst create --project-name my-store --hosting commerce`
|
|
1646
|
-
).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",
|
|
2095
|
+
).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(
|
|
1647
2096
|
"--gh-ref <ref>",
|
|
1648
2097
|
"Clone a specific ref from the source repository",
|
|
1649
2098
|
"@bigcommerce/catalyst-core@latest"
|
|
@@ -1652,7 +2101,7 @@ Examples:
|
|
|
1652
2101
|
"Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable).",
|
|
1653
2102
|
parseEnvFlag
|
|
1654
2103
|
).addOption(
|
|
1655
|
-
new
|
|
2104
|
+
new Option5(
|
|
1656
2105
|
"--hosting <mode>",
|
|
1657
2106
|
'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.'
|
|
1658
2107
|
).choices(["self-hosted", "commerce"])
|
|
@@ -1660,11 +2109,11 @@ Examples:
|
|
|
1660
2109
|
"--use-existing",
|
|
1661
2110
|
"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."
|
|
1662
2111
|
).addOption(
|
|
1663
|
-
new
|
|
2112
|
+
new Option5("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
|
|
1664
2113
|
).addOption(
|
|
1665
|
-
new
|
|
2114
|
+
new Option5("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
|
|
1666
2115
|
).addOption(
|
|
1667
|
-
new
|
|
2116
|
+
new Option5("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
|
|
1668
2117
|
).action(async (options) => {
|
|
1669
2118
|
const { ghRef, repository } = options;
|
|
1670
2119
|
if (options.useExisting && options.hosting !== "commerce") {
|
|
@@ -1681,9 +2130,21 @@ Examples:
|
|
|
1681
2130
|
let storefrontToken = options.storefrontToken;
|
|
1682
2131
|
let envVars = {};
|
|
1683
2132
|
if (!storeHash || !accessToken) {
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
2133
|
+
const apiHost = `api.${options.bigcommerceHostname}`;
|
|
2134
|
+
try {
|
|
2135
|
+
const credentials = await login(options.loginUrl, apiHost);
|
|
2136
|
+
storeHash = credentials.storeHash;
|
|
2137
|
+
accessToken = credentials.accessToken;
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
if (error instanceof LoginAbortedError) {
|
|
2140
|
+
consola.info(
|
|
2141
|
+
"Login aborted. Re-run `catalyst create` when you have your credentials ready."
|
|
2142
|
+
);
|
|
2143
|
+
process.exit(0);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
throw error;
|
|
2147
|
+
}
|
|
1687
2148
|
}
|
|
1688
2149
|
const useCommerceHosting = options.hosting === "commerce";
|
|
1689
2150
|
await getTelemetry().identify(storeHash);
|
|
@@ -1706,7 +2167,7 @@ Examples:
|
|
|
1706
2167
|
}
|
|
1707
2168
|
let shouldCreateChannel;
|
|
1708
2169
|
if (eligibility.eligible) {
|
|
1709
|
-
shouldCreateChannel = await
|
|
2170
|
+
shouldCreateChannel = await select4({
|
|
1710
2171
|
message: "Would you like to create a new channel?",
|
|
1711
2172
|
choices: [
|
|
1712
2173
|
{ name: "Yes", value: true },
|
|
@@ -1769,7 +2230,7 @@ Examples:
|
|
|
1769
2230
|
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
|
|
1770
2231
|
setupCoreProject(projectDir);
|
|
1771
2232
|
if (useCommerceHosting && commerceHostingProject && storeHash && accessToken) {
|
|
1772
|
-
setupCommerceHosting({
|
|
2233
|
+
await setupCommerceHosting({
|
|
1773
2234
|
projectDir,
|
|
1774
2235
|
projectUuid: commerceHostingProject.uuid,
|
|
1775
2236
|
storeHash,
|
|
@@ -1788,26 +2249,28 @@ Examples:
|
|
|
1788
2249
|
throw error;
|
|
1789
2250
|
}
|
|
1790
2251
|
consola.success(`Created '${projectName}' at '${projectDir}'`);
|
|
1791
|
-
|
|
1792
|
-
consola.info(colorize4("yellow", ` cd ${projectName}/core && pnpm run dev`));
|
|
2252
|
+
const steps = [`cd ${projectName}/core && pnpm run dev`];
|
|
1793
2253
|
if (useCommerceHosting) {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
"yellow",
|
|
1797
|
-
` Run 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce Hosting.`
|
|
1798
|
-
)
|
|
2254
|
+
steps.push(
|
|
2255
|
+
`Run 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce Hosting.`
|
|
1799
2256
|
);
|
|
1800
2257
|
}
|
|
2258
|
+
consola.log(
|
|
2259
|
+
`Next steps:
|
|
2260
|
+
|
|
2261
|
+
${steps.map((step) => ` ${colorize4("yellow", step)}`).join("\n")}`
|
|
2262
|
+
);
|
|
1801
2263
|
});
|
|
1802
2264
|
|
|
1803
2265
|
// src/cli/commands/deploy.ts
|
|
2266
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1804
2267
|
import AdmZip from "adm-zip";
|
|
1805
|
-
import { Command as
|
|
2268
|
+
import { Command as Command5, Option as Option6 } from "commander";
|
|
1806
2269
|
import { colorize as colorize5 } from "consola/utils";
|
|
1807
2270
|
import { access, readdir, readFile } from "fs/promises";
|
|
1808
|
-
import { dirname as dirname4, join as
|
|
1809
|
-
import
|
|
1810
|
-
import { z as
|
|
2271
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
2272
|
+
import yoctoSpinner4 from "yocto-spinner";
|
|
2273
|
+
import { z as z10 } from "zod";
|
|
1811
2274
|
|
|
1812
2275
|
// src/cli/lib/deployment-errors.ts
|
|
1813
2276
|
var ERROR_MESSAGES = {
|
|
@@ -1825,23 +2288,8 @@ function getDeploymentErrorMessage(code) {
|
|
|
1825
2288
|
return ERROR_MESSAGES[code] ?? `Deployment failed with an unexpected error (code: ${code}). Please try again. If the issue persists, contact support.`;
|
|
1826
2289
|
}
|
|
1827
2290
|
|
|
1828
|
-
// src/cli/lib/resolve-credentials.ts
|
|
1829
|
-
function resolveCredentials(options, config2) {
|
|
1830
|
-
const storeHash = options.storeHash ?? config2.get("storeHash");
|
|
1831
|
-
const accessToken = options.accessToken ?? config2.get("accessToken");
|
|
1832
|
-
if (!storeHash || !accessToken) {
|
|
1833
|
-
consola.error("Missing credentials.");
|
|
1834
|
-
consola.info(
|
|
1835
|
-
"Run `catalyst auth login`, or provide --store-hash and --access-token flags (or set CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables)."
|
|
1836
|
-
);
|
|
1837
|
-
process.exit(1);
|
|
1838
|
-
throw new Error("Missing credentials");
|
|
1839
|
-
}
|
|
1840
|
-
return { storeHash, accessToken };
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
2291
|
// src/cli/commands/deploy.ts
|
|
1844
|
-
var stepsEnum =
|
|
2292
|
+
var stepsEnum = z10.enum([
|
|
1845
2293
|
"initializing",
|
|
1846
2294
|
"downloading",
|
|
1847
2295
|
"unzipping",
|
|
@@ -1859,41 +2307,41 @@ var STEPS = {
|
|
|
1859
2307
|
finalizing: "Finalizing...",
|
|
1860
2308
|
complete: "Complete"
|
|
1861
2309
|
};
|
|
1862
|
-
var UploadSignatureSchema =
|
|
1863
|
-
data:
|
|
1864
|
-
upload_url:
|
|
1865
|
-
upload_uuid:
|
|
2310
|
+
var UploadSignatureSchema = z10.object({
|
|
2311
|
+
data: z10.object({
|
|
2312
|
+
upload_url: z10.url(),
|
|
2313
|
+
upload_uuid: z10.string()
|
|
1866
2314
|
})
|
|
1867
2315
|
});
|
|
1868
|
-
var CreateDeploymentSchema =
|
|
1869
|
-
data:
|
|
1870
|
-
deployment_uuid:
|
|
2316
|
+
var CreateDeploymentSchema = z10.object({
|
|
2317
|
+
data: z10.object({
|
|
2318
|
+
deployment_uuid: z10.uuid()
|
|
1871
2319
|
})
|
|
1872
2320
|
});
|
|
1873
|
-
var ProjectSchema =
|
|
1874
|
-
data:
|
|
1875
|
-
|
|
1876
|
-
uuid:
|
|
1877
|
-
name:
|
|
2321
|
+
var ProjectSchema = z10.object({
|
|
2322
|
+
data: z10.array(
|
|
2323
|
+
z10.object({
|
|
2324
|
+
uuid: z10.uuid(),
|
|
2325
|
+
name: z10.string()
|
|
1878
2326
|
})
|
|
1879
2327
|
)
|
|
1880
2328
|
});
|
|
1881
|
-
var DeploymentStatusSchema =
|
|
1882
|
-
deployment_uuid:
|
|
1883
|
-
deployment_status:
|
|
1884
|
-
event:
|
|
2329
|
+
var DeploymentStatusSchema = z10.object({
|
|
2330
|
+
deployment_uuid: z10.uuid(),
|
|
2331
|
+
deployment_status: z10.enum(["queued", "in_progress", "failed", "completed"]),
|
|
2332
|
+
event: z10.object({
|
|
1885
2333
|
step: stepsEnum,
|
|
1886
|
-
progress:
|
|
2334
|
+
progress: z10.number()
|
|
1887
2335
|
}).nullable(),
|
|
1888
|
-
|
|
1889
|
-
error:
|
|
1890
|
-
code:
|
|
2336
|
+
deployment_hostnames: z10.array(z10.string()).optional(),
|
|
2337
|
+
error: z10.object({
|
|
2338
|
+
code: z10.number()
|
|
1891
2339
|
}).optional()
|
|
1892
2340
|
});
|
|
1893
2341
|
var generateBundleZip = async () => {
|
|
1894
2342
|
consola.info("Generating bundle...");
|
|
1895
|
-
const bigcommerceDir =
|
|
1896
|
-
const distDir =
|
|
2343
|
+
const bigcommerceDir = join11(process.cwd(), ".bigcommerce");
|
|
2344
|
+
const distDir = join11(process.cwd(), ".bigcommerce", "dist");
|
|
1897
2345
|
try {
|
|
1898
2346
|
await access(distDir);
|
|
1899
2347
|
} catch {
|
|
@@ -1903,7 +2351,7 @@ var generateBundleZip = async () => {
|
|
|
1903
2351
|
if (buildDirContents.length === 0) {
|
|
1904
2352
|
throw new Error(`Dist directory is empty: ${distDir}`);
|
|
1905
2353
|
}
|
|
1906
|
-
const outputZip =
|
|
2354
|
+
const outputZip = join11(bigcommerceDir, "bundle.zip");
|
|
1907
2355
|
const zip = new AdmZip();
|
|
1908
2356
|
zip.addLocalFolder(distDir, "output");
|
|
1909
2357
|
zip.writeZip(outputZip);
|
|
@@ -1924,6 +2372,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
|
|
|
1924
2372
|
body: JSON.stringify({})
|
|
1925
2373
|
}
|
|
1926
2374
|
);
|
|
2375
|
+
assertAuthorized(response);
|
|
1927
2376
|
if (!response.ok) {
|
|
1928
2377
|
throw new Error(`Failed to fetch upload signature: ${response.status} ${response.statusText}`);
|
|
1929
2378
|
}
|
|
@@ -1934,7 +2383,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
|
|
|
1934
2383
|
};
|
|
1935
2384
|
var uploadBundleZip = async (uploadUrl) => {
|
|
1936
2385
|
consola.info("Uploading bundle...");
|
|
1937
|
-
const zipPath =
|
|
2386
|
+
const zipPath = join11(process.cwd(), ".bigcommerce", "bundle.zip");
|
|
1938
2387
|
const fileBuffer = await readFile(zipPath);
|
|
1939
2388
|
const response = await fetch(uploadUrl, {
|
|
1940
2389
|
method: "PUT",
|
|
@@ -1951,41 +2400,14 @@ var uploadBundleZip = async (uploadUrl) => {
|
|
|
1951
2400
|
};
|
|
1952
2401
|
var parseEnvironmentVariables = (secretOption) => {
|
|
1953
2402
|
return secretOption?.map((envVar) => {
|
|
1954
|
-
const
|
|
1955
|
-
if (!key || !value) {
|
|
1956
|
-
throw new Error(`Invalid secret format: ${envVar}. Expected format: KEY=VALUE`);
|
|
1957
|
-
}
|
|
2403
|
+
const { key, value } = parseEnvAssignment(envVar);
|
|
1958
2404
|
return {
|
|
1959
2405
|
type: "secret",
|
|
1960
|
-
key
|
|
1961
|
-
value
|
|
2406
|
+
key,
|
|
2407
|
+
value
|
|
1962
2408
|
};
|
|
1963
2409
|
});
|
|
1964
2410
|
};
|
|
1965
|
-
var AUTO_DETECT_SECRETS = [
|
|
1966
|
-
{ key: "BIGCOMMERCE_STORE_HASH", warnIfMissing: true },
|
|
1967
|
-
{ key: "BIGCOMMERCE_CHANNEL_ID", warnIfMissing: true },
|
|
1968
|
-
{ key: "BIGCOMMERCE_STOREFRONT_TOKEN", warnIfMissing: true },
|
|
1969
|
-
{ key: "BIGCOMMERCE_API_HOST", warnIfMissing: false },
|
|
1970
|
-
{ key: "BIGCOMMERCE_GRAPHQL_API_DOMAIN", warnIfMissing: false },
|
|
1971
|
-
{ key: "AUTH_SECRET", warnIfMissing: false }
|
|
1972
|
-
];
|
|
1973
|
-
var autoDetectSecrets = (environmentVariables) => {
|
|
1974
|
-
const secrets = environmentVariables ?? [];
|
|
1975
|
-
const existingKeys = new Set(secrets.map((s) => s.key));
|
|
1976
|
-
AUTO_DETECT_SECRETS.forEach(({ key, warnIfMissing }) => {
|
|
1977
|
-
if (existingKeys.has(key)) {
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
const value = process.env[key];
|
|
1981
|
-
if (value) {
|
|
1982
|
-
secrets.push({ type: "secret", key, value });
|
|
1983
|
-
} else if (warnIfMissing) {
|
|
1984
|
-
consola.warn(`${key} is not set in the environment and was not provided via --secret.`);
|
|
1985
|
-
}
|
|
1986
|
-
});
|
|
1987
|
-
return secrets;
|
|
1988
|
-
};
|
|
1989
2411
|
var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, apiHost, environmentVariables) => {
|
|
1990
2412
|
consola.info("Creating deployment...");
|
|
1991
2413
|
const response = await fetch(
|
|
@@ -2005,6 +2427,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
|
|
|
2005
2427
|
})
|
|
2006
2428
|
}
|
|
2007
2429
|
);
|
|
2430
|
+
assertAuthorized(response);
|
|
2008
2431
|
if (!response.ok) {
|
|
2009
2432
|
throw new Error(`Failed to create deployment: ${response.status} ${response.statusText}`);
|
|
2010
2433
|
}
|
|
@@ -2015,7 +2438,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
|
|
|
2015
2438
|
};
|
|
2016
2439
|
var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost) => {
|
|
2017
2440
|
consola.info("Fetching deployment status...");
|
|
2018
|
-
const spinner =
|
|
2441
|
+
const spinner = yoctoSpinner4().start("Fetching...");
|
|
2019
2442
|
const response = await fetch(
|
|
2020
2443
|
`https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments/${deploymentUuid}/events`,
|
|
2021
2444
|
{
|
|
@@ -2028,6 +2451,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
|
|
|
2028
2451
|
}
|
|
2029
2452
|
}
|
|
2030
2453
|
);
|
|
2454
|
+
assertAuthorized(response);
|
|
2031
2455
|
if (!response.ok) {
|
|
2032
2456
|
throw new Error(`Failed to open event stream: ${response.status} ${response.statusText}`);
|
|
2033
2457
|
}
|
|
@@ -2037,7 +2461,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
|
|
|
2037
2461
|
}
|
|
2038
2462
|
const decoder = new TextDecoder();
|
|
2039
2463
|
let done = false;
|
|
2040
|
-
let
|
|
2464
|
+
let deploymentHostname;
|
|
2041
2465
|
while (!done) {
|
|
2042
2466
|
const { value, done: streamDone } = await reader.read();
|
|
2043
2467
|
let json;
|
|
@@ -2060,18 +2484,20 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
|
|
|
2060
2484
|
if (data.event && STEPS[data.event.step] !== spinner.text) {
|
|
2061
2485
|
spinner.text = STEPS[data.event.step];
|
|
2062
2486
|
}
|
|
2063
|
-
if (data.
|
|
2064
|
-
|
|
2487
|
+
if (data.deployment_hostnames && data.deployment_hostnames.length > 0) {
|
|
2488
|
+
deploymentHostname = data.deployment_hostnames[0];
|
|
2065
2489
|
}
|
|
2066
2490
|
});
|
|
2067
2491
|
}
|
|
2068
2492
|
done = streamDone;
|
|
2069
2493
|
}
|
|
2070
2494
|
spinner.success("Deployment completed successfully.");
|
|
2071
|
-
if (
|
|
2072
|
-
|
|
2073
|
-
|
|
2495
|
+
if (deploymentHostname) {
|
|
2496
|
+
consola.success(
|
|
2497
|
+
`View your deployment at: ${colorize5("blue", `https://${deploymentHostname}`)}`
|
|
2498
|
+
);
|
|
2074
2499
|
}
|
|
2500
|
+
return deploymentHostname;
|
|
2075
2501
|
};
|
|
2076
2502
|
var fetchProject = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
2077
2503
|
const response = await fetch(
|
|
@@ -2086,6 +2512,7 @@ var fetchProject = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
|
2086
2512
|
}
|
|
2087
2513
|
}
|
|
2088
2514
|
);
|
|
2515
|
+
assertAuthorized(response);
|
|
2089
2516
|
if (!response.ok) {
|
|
2090
2517
|
throw new Error(`Failed to fetch projects: ${response.status} ${response.statusText}`);
|
|
2091
2518
|
}
|
|
@@ -2093,15 +2520,21 @@ var fetchProject = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
|
2093
2520
|
const { data } = ProjectSchema.parse(res);
|
|
2094
2521
|
return data.find((project2) => project2.uuid === projectUuid);
|
|
2095
2522
|
};
|
|
2096
|
-
var deploy = new
|
|
2523
|
+
var deploy = new Command5("deploy").configureHelp({ showGlobalOptions: true }).description("Deploy your application to Cloudflare.").addHelpText(
|
|
2097
2524
|
"after",
|
|
2098
2525
|
`
|
|
2526
|
+
Environment variables saved with \`catalyst env add\` are sent automatically on every deploy.
|
|
2527
|
+
Use \`--secret\` to set or override a variable for a single run.
|
|
2528
|
+
|
|
2099
2529
|
Example:
|
|
2100
2530
|
$ catalyst deploy --secret BIGCOMMERCE_STORE_HASH=<YOUR_STORE_HASH> --secret BIGCOMMERCE_STOREFRONT_TOKEN=<YOUR_STOREFRONT_TOKEN>`
|
|
2101
|
-
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).
|
|
2102
|
-
|
|
2531
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option(
|
|
2532
|
+
"--update-site-url",
|
|
2533
|
+
"After a successful deploy, prompt to update a channel's site URL to the new hostname."
|
|
2534
|
+
).addOption(
|
|
2535
|
+
new Option6(
|
|
2103
2536
|
"--secret <value>",
|
|
2104
|
-
"Secret to set for
|
|
2537
|
+
"Secret to set for this deployment (repeatable). Overrides a stored value for the same key. Format: --secret KEY=VALUE"
|
|
2105
2538
|
).argParser((value, previous = []) => {
|
|
2106
2539
|
return previous.concat([value]);
|
|
2107
2540
|
})
|
|
@@ -2158,21 +2591,23 @@ Example:
|
|
|
2158
2591
|
consola.success(`Linked project "${selected.name}".`);
|
|
2159
2592
|
}
|
|
2160
2593
|
if (!getProjectState().isTransformed) {
|
|
2161
|
-
const shouldSetup = await
|
|
2162
|
-
"Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
|
|
2163
|
-
|
|
2164
|
-
);
|
|
2594
|
+
const shouldSetup = await confirm3({
|
|
2595
|
+
message: "Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
|
|
2596
|
+
default: true
|
|
2597
|
+
});
|
|
2165
2598
|
if (!shouldSetup) {
|
|
2166
2599
|
consola.info("When you're ready to deploy, re-run `catalyst deploy` to complete setup.");
|
|
2167
2600
|
process.exit(0);
|
|
2168
2601
|
}
|
|
2169
2602
|
const projectDir = dirname4(process.cwd());
|
|
2170
|
-
setupCommerceHosting({ projectDir, projectUuid, storeHash, accessToken });
|
|
2603
|
+
await setupCommerceHosting({ projectDir, projectUuid, storeHash, accessToken });
|
|
2171
2604
|
consola.success("Commerce Hosting setup complete.");
|
|
2172
2605
|
await installDependencies(projectDir);
|
|
2606
|
+
} else {
|
|
2607
|
+
await cleanupCloudflareIncompatibilities(dirname4(process.cwd()));
|
|
2173
2608
|
}
|
|
2174
2609
|
if (options.prebuilt) {
|
|
2175
|
-
const distDir =
|
|
2610
|
+
const distDir = join11(process.cwd(), ".bigcommerce", "dist");
|
|
2176
2611
|
try {
|
|
2177
2612
|
await access(distDir);
|
|
2178
2613
|
} catch {
|
|
@@ -2201,7 +2636,10 @@ Example:
|
|
|
2201
2636
|
}
|
|
2202
2637
|
const uploadSignature = await generateUploadSignature(storeHash, accessToken, options.apiHost);
|
|
2203
2638
|
await uploadBundleZip(uploadSignature.upload_url);
|
|
2204
|
-
const
|
|
2639
|
+
const flagSecrets = parseEnvironmentVariables(options.secret) ?? [];
|
|
2640
|
+
const persistedSecrets = toDeploymentSecrets(getStoredEnv(config2));
|
|
2641
|
+
const mergedSecrets = mergeDeploymentSecrets(persistedSecrets, flagSecrets);
|
|
2642
|
+
const environmentVariables = mergedSecrets.length > 0 ? mergedSecrets : void 0;
|
|
2205
2643
|
const { deployment_uuid: deploymentUuid } = await createDeployment(
|
|
2206
2644
|
projectUuid,
|
|
2207
2645
|
uploadSignature.upload_uuid,
|
|
@@ -2210,13 +2648,109 @@ Example:
|
|
|
2210
2648
|
options.apiHost,
|
|
2211
2649
|
environmentVariables
|
|
2212
2650
|
);
|
|
2213
|
-
await getDeploymentStatus(
|
|
2651
|
+
const deploymentHostname = await getDeploymentStatus(
|
|
2652
|
+
deploymentUuid,
|
|
2653
|
+
storeHash,
|
|
2654
|
+
accessToken,
|
|
2655
|
+
options.apiHost
|
|
2656
|
+
);
|
|
2657
|
+
if (!options.updateSiteUrl) {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
try {
|
|
2661
|
+
await runChannelSiteUrlFlow({
|
|
2662
|
+
storeHash,
|
|
2663
|
+
accessToken,
|
|
2664
|
+
apiHost: options.apiHost,
|
|
2665
|
+
projectUuid,
|
|
2666
|
+
preferHostname: deploymentHostname
|
|
2667
|
+
});
|
|
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
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
|
|
2678
|
+
// src/cli/commands/env.ts
|
|
2679
|
+
import { Command as Command6 } from "commander";
|
|
2680
|
+
var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2681
|
+
var add = new Command6("add").configureHelp({ showGlobalOptions: true }).description(
|
|
2682
|
+
"Add or update one or more deployment environment variables. Stored in .bigcommerce/project.json and sent as secrets on every `catalyst deploy`."
|
|
2683
|
+
).argument("<vars...>", "One or more environment variables in KEY=VALUE format.").addHelpText(
|
|
2684
|
+
"after",
|
|
2685
|
+
`
|
|
2686
|
+
Example:
|
|
2687
|
+
$ catalyst env add BIGCOMMERCE_STORE_HASH=abc123 BIGCOMMERCE_STOREFRONT_TOKEN=ey...`
|
|
2688
|
+
).action((vars) => {
|
|
2689
|
+
const config2 = getProjectConfig();
|
|
2690
|
+
const stored = { ...getStoredEnv(config2) };
|
|
2691
|
+
const parsed = vars.map((entry) => parseEnvAssignment(entry));
|
|
2692
|
+
parsed.forEach(({ key, value }) => {
|
|
2693
|
+
stored[key] = value;
|
|
2694
|
+
});
|
|
2695
|
+
config2.set("env", stored);
|
|
2696
|
+
const keys = parsed.map(({ key }) => key);
|
|
2697
|
+
consola.success(
|
|
2698
|
+
`Saved ${keys.length} environment variable${keys.length === 1 ? "" : "s"} to .bigcommerce/project.json:`
|
|
2699
|
+
);
|
|
2700
|
+
keys.forEach((key) => consola.log(` ${key}=${MASK}`));
|
|
2701
|
+
process.exit(0);
|
|
2702
|
+
});
|
|
2703
|
+
var remove = new Command6("remove").configureHelp({ showGlobalOptions: true }).description("Remove one or more stored deployment environment variables.").argument("<keys...>", "One or more environment variable names to remove.").addHelpText(
|
|
2704
|
+
"after",
|
|
2705
|
+
`
|
|
2706
|
+
Example:
|
|
2707
|
+
$ catalyst env remove BIGCOMMERCE_STORE_HASH BIGCOMMERCE_STOREFRONT_TOKEN`
|
|
2708
|
+
).action((keys) => {
|
|
2709
|
+
const config2 = getProjectConfig();
|
|
2710
|
+
const stored = getStoredEnv(config2);
|
|
2711
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2712
|
+
keys.forEach((key) => {
|
|
2713
|
+
if (key in stored) {
|
|
2714
|
+
toRemove.add(key);
|
|
2715
|
+
} else {
|
|
2716
|
+
consola.warn(`No stored environment variable named "${key}". Skipping.`);
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
const next = Object.fromEntries(Object.entries(stored).filter(([key]) => !toRemove.has(key)));
|
|
2720
|
+
config2.set("env", next);
|
|
2721
|
+
const removed = Array.from(toRemove);
|
|
2722
|
+
if (removed.length > 0) {
|
|
2723
|
+
consola.success(
|
|
2724
|
+
`Removed ${removed.length} environment variable${removed.length === 1 ? "" : "s"}: ${removed.join(", ")}.`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
process.exit(0);
|
|
2728
|
+
});
|
|
2729
|
+
var list = new Command6("list").configureHelp({ showGlobalOptions: true }).description("List stored deployment environment variables (values are masked).").addHelpText(
|
|
2730
|
+
"after",
|
|
2731
|
+
`
|
|
2732
|
+
Example:
|
|
2733
|
+
$ catalyst env list`
|
|
2734
|
+
).action(() => {
|
|
2735
|
+
const config2 = getProjectConfig();
|
|
2736
|
+
const stored = getStoredEnv(config2);
|
|
2737
|
+
const keys = Object.keys(stored).sort();
|
|
2738
|
+
if (keys.length === 0) {
|
|
2739
|
+
consola.info("No environment variables stored. Add one with `catalyst env add KEY=VALUE`.");
|
|
2740
|
+
process.exit(0);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
keys.forEach((key) => consola.log(`${key}=${MASK}`));
|
|
2744
|
+
process.exit(0);
|
|
2214
2745
|
});
|
|
2746
|
+
var env = new Command6("env").configureHelp({ showGlobalOptions: true }).description(
|
|
2747
|
+
"Manage persistent deployment environment variables. These are sent as secrets on every `catalyst deploy`, so you no longer need to pass `--secret` each time."
|
|
2748
|
+
).addCommand(add).addCommand(remove).addCommand(list);
|
|
2215
2749
|
|
|
2216
2750
|
// src/cli/commands/logs.ts
|
|
2217
|
-
import { Command as
|
|
2751
|
+
import { Command as Command7, Option as Option7 } from "commander";
|
|
2218
2752
|
import { colorize as colorize6 } from "consola/utils";
|
|
2219
|
-
import { z as
|
|
2753
|
+
import { z as z11 } from "zod";
|
|
2220
2754
|
var telemetry = new Telemetry();
|
|
2221
2755
|
var DEFAULT_CONNECTION_TTL_MS = 1 * 60 * 1e3;
|
|
2222
2756
|
var MAX_RETRIES = 5;
|
|
@@ -2227,23 +2761,23 @@ var LEVEL_COLORS = {
|
|
|
2227
2761
|
ERROR: "red",
|
|
2228
2762
|
DEBUG: "gray"
|
|
2229
2763
|
};
|
|
2230
|
-
var LogEventSchema =
|
|
2231
|
-
uuid:
|
|
2232
|
-
project_uuid:
|
|
2233
|
-
request:
|
|
2234
|
-
method:
|
|
2235
|
-
url:
|
|
2236
|
-
status_code:
|
|
2764
|
+
var LogEventSchema = z11.object({
|
|
2765
|
+
uuid: z11.string(),
|
|
2766
|
+
project_uuid: z11.string(),
|
|
2767
|
+
request: z11.object({
|
|
2768
|
+
method: z11.string(),
|
|
2769
|
+
url: z11.string(),
|
|
2770
|
+
status_code: z11.number()
|
|
2237
2771
|
}),
|
|
2238
|
-
logs:
|
|
2239
|
-
|
|
2240
|
-
timestamp:
|
|
2241
|
-
level:
|
|
2242
|
-
messages:
|
|
2772
|
+
logs: z11.array(
|
|
2773
|
+
z11.object({
|
|
2774
|
+
timestamp: z11.string(),
|
|
2775
|
+
level: z11.string(),
|
|
2776
|
+
messages: z11.array(z11.unknown())
|
|
2243
2777
|
})
|
|
2244
2778
|
),
|
|
2245
|
-
exceptions:
|
|
2246
|
-
timestamp:
|
|
2779
|
+
exceptions: z11.array(z11.unknown()),
|
|
2780
|
+
timestamp: z11.string()
|
|
2247
2781
|
}).loose();
|
|
2248
2782
|
var StreamError = class extends Error {
|
|
2249
2783
|
constructor(message, fatal) {
|
|
@@ -2298,6 +2832,20 @@ var processLogEvent = (event, format) => {
|
|
|
2298
2832
|
consola.warn(`Failed to parse log event: ${event}`);
|
|
2299
2833
|
}
|
|
2300
2834
|
};
|
|
2835
|
+
var raceWithTimeout = async (promise, timeoutMs) => {
|
|
2836
|
+
let timeoutHandle;
|
|
2837
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
2838
|
+
timeoutHandle = setTimeout(() => resolve2({ kind: "timeout" }), timeoutMs);
|
|
2839
|
+
});
|
|
2840
|
+
try {
|
|
2841
|
+
return await Promise.race([
|
|
2842
|
+
promise.then((value) => ({ kind: "value", value })),
|
|
2843
|
+
timeoutPromise
|
|
2844
|
+
]);
|
|
2845
|
+
} finally {
|
|
2846
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2301
2849
|
var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
2302
2850
|
const response = await fetch(
|
|
2303
2851
|
`https://${apiHost}/stores/${storeHash}/v3/infrastructure/logs/${projectUuid}/tail`,
|
|
@@ -2310,6 +2858,9 @@ var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
|
2310
2858
|
}
|
|
2311
2859
|
}
|
|
2312
2860
|
);
|
|
2861
|
+
if (response.status === 401) {
|
|
2862
|
+
throw new StreamError(new UnauthorizedError().message, true);
|
|
2863
|
+
}
|
|
2313
2864
|
if (!response.ok) {
|
|
2314
2865
|
throw new StreamError(
|
|
2315
2866
|
`Failed to open log stream: ${response.status} ${response.statusText}`,
|
|
@@ -2322,28 +2873,46 @@ var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
|
2322
2873
|
}
|
|
2323
2874
|
return reader;
|
|
2324
2875
|
};
|
|
2876
|
+
var pumpUntilRotation = async (reader, format, connectionTtlMs) => {
|
|
2877
|
+
const decoder = new TextDecoder();
|
|
2878
|
+
const connectTime = Date.now();
|
|
2879
|
+
let buffer = "";
|
|
2880
|
+
let receivedData = false;
|
|
2881
|
+
while (true) {
|
|
2882
|
+
const remainingTtlMs = connectionTtlMs - (Date.now() - connectTime);
|
|
2883
|
+
if (remainingTtlMs <= 0) {
|
|
2884
|
+
void reader.cancel();
|
|
2885
|
+
return "ttl";
|
|
2886
|
+
}
|
|
2887
|
+
const readResult = await raceWithTimeout(reader.read(), remainingTtlMs);
|
|
2888
|
+
if (readResult.kind === "timeout") {
|
|
2889
|
+
void reader.cancel();
|
|
2890
|
+
return receivedData ? "ttl" : "idle-timeout";
|
|
2891
|
+
}
|
|
2892
|
+
const { value, done: streamDone } = readResult.value;
|
|
2893
|
+
if (value) {
|
|
2894
|
+
receivedData = true;
|
|
2895
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2896
|
+
const parts = buffer.split("\n\n");
|
|
2897
|
+
buffer = parts.pop() ?? "";
|
|
2898
|
+
parts.map((raw) => parseSSEEvent(raw)).filter((event) => event !== null).forEach((event) => processLogEvent(event, format));
|
|
2899
|
+
}
|
|
2900
|
+
if (streamDone) {
|
|
2901
|
+
void reader.cancel();
|
|
2902
|
+
return "stream-done";
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2325
2906
|
var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, connectionTtlMs = DEFAULT_CONNECTION_TTL_MS) => {
|
|
2326
2907
|
consola.info("Tailing logs...");
|
|
2327
2908
|
let retries = 0;
|
|
2328
2909
|
while (true) {
|
|
2329
2910
|
try {
|
|
2330
2911
|
const reader = await openLogStream(projectUuid, storeHash, accessToken, apiHost);
|
|
2331
|
-
const decoder = new TextDecoder();
|
|
2332
|
-
const connectTime = Date.now();
|
|
2333
|
-
let buffer = "";
|
|
2334
2912
|
retries = 0;
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
buffer += decoder.decode(value, { stream: true });
|
|
2339
|
-
const parts = buffer.split("\n\n");
|
|
2340
|
-
buffer = parts.pop() ?? "";
|
|
2341
|
-
parts.map((raw) => parseSSEEvent(raw)).filter((event) => event !== null).forEach((event) => processLogEvent(event, format));
|
|
2342
|
-
}
|
|
2343
|
-
if (streamDone || Date.now() - connectTime >= connectionTtlMs) {
|
|
2344
|
-
void reader.cancel();
|
|
2345
|
-
break;
|
|
2346
|
-
}
|
|
2913
|
+
const rotation = await pumpUntilRotation(reader, format, connectionTtlMs);
|
|
2914
|
+
if (rotation === "idle-timeout") {
|
|
2915
|
+
consola.warn("Log stream idle, reconnecting...");
|
|
2347
2916
|
}
|
|
2348
2917
|
} catch (error) {
|
|
2349
2918
|
if (error instanceof StreamError && error.fatal) {
|
|
@@ -2365,7 +2934,7 @@ var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, conn
|
|
|
2365
2934
|
}
|
|
2366
2935
|
}
|
|
2367
2936
|
};
|
|
2368
|
-
var tail = new
|
|
2937
|
+
var tail = new Command7("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
|
|
2369
2938
|
"after",
|
|
2370
2939
|
`
|
|
2371
2940
|
Examples:
|
|
@@ -2377,7 +2946,7 @@ Examples:
|
|
|
2377
2946
|
# Tail logs as raw JSON (useful for piping to other tools)
|
|
2378
2947
|
$ catalyst logs tail --format json`
|
|
2379
2948
|
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
|
|
2380
|
-
new
|
|
2949
|
+
new Option7("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
|
|
2381
2950
|
).action(async (options) => {
|
|
2382
2951
|
try {
|
|
2383
2952
|
const config2 = getProjectConfig();
|
|
@@ -2390,7 +2959,7 @@ Examples:
|
|
|
2390
2959
|
process.exit(1);
|
|
2391
2960
|
}
|
|
2392
2961
|
});
|
|
2393
|
-
var query = new
|
|
2962
|
+
var query = new Command7("query").configureHelp({ showGlobalOptions: true }).description("Query historical logs from your deployed application.").addHelpText(
|
|
2394
2963
|
"after",
|
|
2395
2964
|
`
|
|
2396
2965
|
Example:
|
|
@@ -2401,21 +2970,25 @@ Example:
|
|
|
2401
2970
|
consola.error("The query command is not yet implemented.");
|
|
2402
2971
|
process.exit(1);
|
|
2403
2972
|
});
|
|
2404
|
-
var logs = new
|
|
2973
|
+
var logs = new Command7("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
|
|
2405
2974
|
|
|
2406
2975
|
// src/cli/commands/project.ts
|
|
2407
|
-
import {
|
|
2976
|
+
import { confirm as confirm4, input as input4, select as select5 } from "@inquirer/prompts";
|
|
2977
|
+
import { Command as Command8 } from "commander";
|
|
2408
2978
|
import { colorize as colorize7 } from "consola/utils";
|
|
2409
2979
|
import { dirname as dirname5 } from "path";
|
|
2410
2980
|
async function offerCommerceHostingSetup(projectUuid, credentials) {
|
|
2411
|
-
if (getProjectState().isTransformed) return;
|
|
2412
|
-
const shouldSetup = await consola.prompt(
|
|
2413
|
-
"Your project has been linked, but is not fully set up for Commerce Hosting deployments yet. Would you like to run the setup now?",
|
|
2414
|
-
{ type: "confirm", initial: true }
|
|
2415
|
-
);
|
|
2416
|
-
if (!shouldSetup) return;
|
|
2417
2981
|
const projectDir = dirname5(process.cwd());
|
|
2418
|
-
|
|
2982
|
+
if (getProjectState().isTransformed) {
|
|
2983
|
+
await cleanupCloudflareIncompatibilities(projectDir);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
const shouldSetup = await confirm4({
|
|
2987
|
+
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
|
+
default: true
|
|
2989
|
+
});
|
|
2990
|
+
if (!shouldSetup) return;
|
|
2991
|
+
await setupCommerceHosting({
|
|
2419
2992
|
projectDir,
|
|
2420
2993
|
projectUuid,
|
|
2421
2994
|
storeHash: credentials?.storeHash,
|
|
@@ -2424,7 +2997,7 @@ async function offerCommerceHostingSetup(projectUuid, credentials) {
|
|
|
2424
2997
|
consola.success("Commerce Hosting setup complete.");
|
|
2425
2998
|
await installDependencies(projectDir);
|
|
2426
2999
|
}
|
|
2427
|
-
var
|
|
3000
|
+
var list2 = new Command8("list").configureHelp({ showGlobalOptions: true }).description("List BigCommerce infrastructure projects for your store.").addHelpText(
|
|
2428
3001
|
"after",
|
|
2429
3002
|
`
|
|
2430
3003
|
Example:
|
|
@@ -2442,26 +3015,51 @@ Example:
|
|
|
2442
3015
|
return;
|
|
2443
3016
|
}
|
|
2444
3017
|
const linkedProjectUuid = config2.get("projectUuid");
|
|
2445
|
-
projects.
|
|
3018
|
+
const projectList = projects.map((p) => {
|
|
2446
3019
|
const marker = p.uuid === linkedProjectUuid ? ` ${colorize7("green", "[linked]")}` : "";
|
|
2447
|
-
|
|
3020
|
+
const hostnames = p.deployment_hostnames.length === 0 ? [" (not deployed)"] : p.deployment_hostnames.map(
|
|
3021
|
+
(hostname) => ` ${colorize7("blue", `https://${hostname}`)}`
|
|
3022
|
+
);
|
|
3023
|
+
return [`${p.name} (${p.uuid})${marker}`, ...hostnames].join("\n");
|
|
2448
3024
|
});
|
|
3025
|
+
consola.log(projectList.join("\n\n"));
|
|
2449
3026
|
process.exit(0);
|
|
2450
3027
|
});
|
|
2451
|
-
var create2 = new
|
|
2452
|
-
"Create a new BigCommerce infrastructure project and link it to your local Catalyst project."
|
|
3028
|
+
var create2 = new Command8("create").configureHelp({ showGlobalOptions: true }).description(
|
|
3029
|
+
"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."
|
|
2453
3030
|
).addHelpText(
|
|
2454
3031
|
"after",
|
|
2455
3032
|
`
|
|
2456
3033
|
Example:
|
|
2457
3034
|
$ catalyst project create`
|
|
2458
|
-
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).action(async (options) => {
|
|
3035
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).action(async (options) => {
|
|
2459
3036
|
const config2 = getProjectConfig();
|
|
2460
|
-
|
|
3037
|
+
let storeHash = options.storeHash ?? config2.get("storeHash");
|
|
3038
|
+
let accessToken = options.accessToken ?? config2.get("accessToken");
|
|
3039
|
+
if (!storeHash || !accessToken) {
|
|
3040
|
+
consola.info(
|
|
3041
|
+
"You're not logged in yet. Let's get you authenticated before creating a project."
|
|
3042
|
+
);
|
|
3043
|
+
try {
|
|
3044
|
+
const credentials = await login(options.loginUrl, options.apiHost);
|
|
3045
|
+
storeHash = credentials.storeHash;
|
|
3046
|
+
accessToken = credentials.accessToken;
|
|
3047
|
+
config2.set("storeHash", storeHash);
|
|
3048
|
+
config2.set("accessToken", accessToken);
|
|
3049
|
+
consola.success(`Logged in to store ${storeHash}.`);
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
if (error instanceof LoginAbortedError) {
|
|
3052
|
+
consola.info(
|
|
3053
|
+
"Login aborted. Re-run `catalyst project create` when you have your credentials ready."
|
|
3054
|
+
);
|
|
3055
|
+
process.exit(0);
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
throw error;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
2461
3061
|
await getTelemetry().identify(storeHash);
|
|
2462
|
-
const newProjectName = await
|
|
2463
|
-
type: "text"
|
|
2464
|
-
});
|
|
3062
|
+
const newProjectName = await input4({ message: "Enter a name for the new project:" });
|
|
2465
3063
|
const data = await createProject(newProjectName, storeHash, accessToken, options.apiHost);
|
|
2466
3064
|
consola.success(`Project "${data.name}" created successfully.`);
|
|
2467
3065
|
consola.start("Writing project UUID to .bigcommerce/project.json...");
|
|
@@ -2471,7 +3069,7 @@ Example:
|
|
|
2471
3069
|
consola.success("Project UUID written to .bigcommerce/project.json.");
|
|
2472
3070
|
process.exit(0);
|
|
2473
3071
|
});
|
|
2474
|
-
var
|
|
3072
|
+
var link2 = new Command8("link").configureHelp({ showGlobalOptions: true }).description(
|
|
2475
3073
|
"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."
|
|
2476
3074
|
).addHelpText(
|
|
2477
3075
|
"after",
|
|
@@ -2521,7 +3119,7 @@ Examples:
|
|
|
2521
3119
|
await offerCommerceHostingSetup(selected.uuid, { storeHash, accessToken });
|
|
2522
3120
|
process.exit(0);
|
|
2523
3121
|
});
|
|
2524
|
-
var del = new
|
|
3122
|
+
var del = new Command8("delete").configureHelp({ showGlobalOptions: true }).description(
|
|
2525
3123
|
"Permanently delete a BigCommerce infrastructure project. This action is irreversible."
|
|
2526
3124
|
).addHelpText(
|
|
2527
3125
|
"after",
|
|
@@ -2551,21 +3149,17 @@ Examples:
|
|
|
2551
3149
|
return;
|
|
2552
3150
|
}
|
|
2553
3151
|
const linkedProjectUuid = config2.get("projectUuid");
|
|
2554
|
-
const selected = await
|
|
2555
|
-
"Select a project to delete (Press <enter> to select).",
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
],
|
|
2566
|
-
cancel: "reject"
|
|
2567
|
-
}
|
|
2568
|
-
);
|
|
3152
|
+
const selected = await select5({
|
|
3153
|
+
message: "Select a project to delete (Press <enter> to select).",
|
|
3154
|
+
choices: [
|
|
3155
|
+
...projects.map((p) => ({
|
|
3156
|
+
name: p.uuid === linkedProjectUuid ? `${p.name} ${colorize7("green", "[linked]")}` : p.name,
|
|
3157
|
+
value: p.uuid,
|
|
3158
|
+
description: p.uuid
|
|
3159
|
+
})),
|
|
3160
|
+
{ name: "Cancel", value: "cancel", description: "Exit without deleting any project." }
|
|
3161
|
+
]
|
|
3162
|
+
});
|
|
2569
3163
|
if (selected === "cancel") {
|
|
2570
3164
|
consola.info("Aborted. No project was deleted.");
|
|
2571
3165
|
process.exit(0);
|
|
@@ -2580,10 +3174,10 @@ Examples:
|
|
|
2580
3174
|
}
|
|
2581
3175
|
if (!options.force) {
|
|
2582
3176
|
const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
|
|
2583
|
-
const confirmed = await
|
|
2584
|
-
`Are you sure you want to delete project ${label}? This action is irreversible and will permanently destroy the project and all of its data.`,
|
|
2585
|
-
|
|
2586
|
-
);
|
|
3177
|
+
const confirmed = await confirm4({
|
|
3178
|
+
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
|
+
default: false
|
|
3180
|
+
});
|
|
2587
3181
|
if (!confirmed) {
|
|
2588
3182
|
consola.info("Aborted. No project was deleted.");
|
|
2589
3183
|
process.exit(0);
|
|
@@ -2599,14 +3193,14 @@ Examples:
|
|
|
2599
3193
|
}
|
|
2600
3194
|
process.exit(0);
|
|
2601
3195
|
});
|
|
2602
|
-
var project = new
|
|
3196
|
+
var project = new Command8("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create2).addCommand(list2).addCommand(link2).addCommand(del);
|
|
2603
3197
|
|
|
2604
3198
|
// src/cli/commands/start.ts
|
|
2605
|
-
import { Command as
|
|
3199
|
+
import { Command as Command9 } from "commander";
|
|
2606
3200
|
import { execa as execa2 } from "execa";
|
|
2607
3201
|
import { existsSync as existsSync4, lstatSync as lstatSync2, symlinkSync as symlinkSync2 } from "fs";
|
|
2608
|
-
import { join as
|
|
2609
|
-
var start = new
|
|
3202
|
+
import { join as join12, relative } from "path";
|
|
3203
|
+
var start = new Command9("start").configureHelp({ showGlobalOptions: true }).description(
|
|
2610
3204
|
"Start a local preview of your Catalyst storefront using the OpenNext Cloudflare adapter."
|
|
2611
3205
|
).addHelpText(
|
|
2612
3206
|
"after",
|
|
@@ -2623,11 +3217,11 @@ Example:
|
|
|
2623
3217
|
});
|
|
2624
3218
|
return;
|
|
2625
3219
|
}
|
|
2626
|
-
const envLocal =
|
|
2627
|
-
const devVars =
|
|
3220
|
+
const envLocal = join12(process.cwd(), ".env.local");
|
|
3221
|
+
const devVars = join12(process.cwd(), ".bigcommerce", ".dev.vars");
|
|
2628
3222
|
if (existsSync4(envLocal)) {
|
|
2629
3223
|
if (!existsSync4(devVars)) {
|
|
2630
|
-
const target = relative(
|
|
3224
|
+
const target = relative(join12(process.cwd(), ".bigcommerce"), envLocal);
|
|
2631
3225
|
symlinkSync2(target, devVars);
|
|
2632
3226
|
consola.success("Symlinked .bigcommerce/.dev.vars \u2192 .env.local");
|
|
2633
3227
|
} else if (!lstatSync2(devVars).isSymbolicLink()) {
|
|
@@ -2647,7 +3241,7 @@ Example:
|
|
|
2647
3241
|
"opennextjs-cloudflare",
|
|
2648
3242
|
"preview",
|
|
2649
3243
|
"--config",
|
|
2650
|
-
|
|
3244
|
+
join12(".bigcommerce", "wrangler.jsonc")
|
|
2651
3245
|
],
|
|
2652
3246
|
{
|
|
2653
3247
|
stdio: "inherit",
|
|
@@ -2657,11 +3251,11 @@ Example:
|
|
|
2657
3251
|
});
|
|
2658
3252
|
|
|
2659
3253
|
// src/cli/commands/telemetry.ts
|
|
2660
|
-
import { Argument, Command as
|
|
3254
|
+
import { Argument, Command as Command10, Option as Option8 } from "commander";
|
|
2661
3255
|
import { colorize as colorize8 } from "consola/utils";
|
|
2662
3256
|
var telemetryService = getTelemetry();
|
|
2663
3257
|
var isEnabled = telemetryService.isEnabled();
|
|
2664
|
-
var telemetry2 = new
|
|
3258
|
+
var telemetry2 = new Command10("telemetry").configureHelp({ showGlobalOptions: true }).description(
|
|
2665
3259
|
"View or change CLI telemetry collection status. Enabling telemetry helps BigCommerce support diagnose and troubleshoot errors you encounter when using the CLI."
|
|
2666
3260
|
).addHelpText(
|
|
2667
3261
|
"after",
|
|
@@ -2675,7 +3269,7 @@ Examples:
|
|
|
2675
3269
|
|
|
2676
3270
|
# Disable telemetry collection
|
|
2677
3271
|
$ catalyst telemetry --disable`
|
|
2678
|
-
).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new
|
|
3272
|
+
).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new Option8("--enable", `Enables CLI telemetry collection.`).conflicts("disable")).option("--disable", `Disables CLI telemetry collection.`).action((arg, options) => {
|
|
2679
3273
|
if (options.enable || arg === "enable") {
|
|
2680
3274
|
telemetryService.setEnabled(true);
|
|
2681
3275
|
isEnabled = true;
|
|
@@ -2702,8 +3296,8 @@ Examples:
|
|
|
2702
3296
|
});
|
|
2703
3297
|
|
|
2704
3298
|
// src/cli/commands/version.ts
|
|
2705
|
-
import { Command as
|
|
2706
|
-
var version = new
|
|
3299
|
+
import { Command as Command11 } from "commander";
|
|
3300
|
+
var version = new Command11("version").configureHelp({ showGlobalOptions: true }).description("Display detailed version information.").addHelpText(
|
|
2707
3301
|
"after",
|
|
2708
3302
|
`
|
|
2709
3303
|
Example:
|
|
@@ -2771,13 +3365,13 @@ if (existsSync5(defaultEnvPath)) {
|
|
|
2771
3365
|
if (!process.env.CATALYST_STORE_HASH && process.env.BIGCOMMERCE_STORE_HASH) {
|
|
2772
3366
|
process.env.CATALYST_STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;
|
|
2773
3367
|
}
|
|
2774
|
-
var program = new
|
|
3368
|
+
var program = new Command12();
|
|
2775
3369
|
consola.log(colorize9("cyanBright", `\u25E2 ${package_default.name} v${package_default.version}
|
|
2776
3370
|
`));
|
|
2777
3371
|
program.name(package_default.name).version(package_default.version).summary("CLI tool for Catalyst development").description(
|
|
2778
3372
|
"CLI tool for Catalyst development.\n\nConfiguration priority: flags > env file (--env-path) > process.env > .env.local (auto-loaded from cwd) > .bigcommerce/project.json.\n\nCATALYST_STORE_HASH falls back to BIGCOMMERCE_STORE_HASH if unset.\n\nRun `catalyst <command> --help` for details on a specific command."
|
|
2779
3373
|
).configureHelp({ showGlobalOptions: true }).addOption(
|
|
2780
|
-
new
|
|
3374
|
+
new Option9(
|
|
2781
3375
|
"--env-path <path>",
|
|
2782
3376
|
"Path to environment file to load (relative to current working directory)"
|
|
2783
3377
|
// We are using argParser, because commander loads in environment variables before executing hooks.
|
|
@@ -2798,7 +3392,7 @@ program.name(package_default.name).version(package_default.version).summary("CLI
|
|
|
2798
3392
|
}
|
|
2799
3393
|
return value;
|
|
2800
3394
|
})
|
|
2801
|
-
).addCommand(version).addCommand(create).addCommand(start).addCommand(build).addCommand(deploy).addCommand(logs).addCommand(project).addCommand(auth).addCommand(telemetry2).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
|
|
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);
|
|
2802
3396
|
|
|
2803
3397
|
// src/cli/index.ts
|
|
2804
3398
|
var handleFatalError = async (error) => {
|
|
@@ -2815,6 +3409,10 @@ var handleFatalError = async (error) => {
|
|
|
2815
3409
|
await telemetry3.analytics.closeAndFlush();
|
|
2816
3410
|
} catch {
|
|
2817
3411
|
}
|
|
3412
|
+
if (error instanceof UnauthorizedError) {
|
|
3413
|
+
consola.error(errorMessage);
|
|
3414
|
+
process.exit(1);
|
|
3415
|
+
}
|
|
2818
3416
|
consola.error(errorMessage);
|
|
2819
3417
|
if (telemetry3.isEnabled()) {
|
|
2820
3418
|
consola.info(
|