@bigcommerce/catalyst 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +2240 -410
- package/dist/cli.js.map +1 -1
- package/package.json +11 -2
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.5",
|
|
17
32
|
type: "module",
|
|
18
33
|
bin: {
|
|
19
34
|
catalyst: "dist/cli.js"
|
|
@@ -35,22 +50,31 @@ var package_default = {
|
|
|
35
50
|
node: "^20.0.0 || ^22.0.0 || ^24.0.0"
|
|
36
51
|
},
|
|
37
52
|
dependencies: {
|
|
53
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
54
|
+
"@inquirer/prompts": "^7.5.3",
|
|
38
55
|
"@segment/analytics-node": "^2.2.1",
|
|
39
56
|
"adm-zip": "^0.5.16",
|
|
40
57
|
commander: "^14.0.0",
|
|
41
58
|
conf: "^13.1.0",
|
|
42
59
|
consola: "^3.4.2",
|
|
60
|
+
"cross-spawn": "^7.0.6",
|
|
43
61
|
dotenv: "^16.5.0",
|
|
44
62
|
execa: "^9.6.0",
|
|
63
|
+
"fs-extra": "^11.3.0",
|
|
64
|
+
"lodash.kebabcase": "^4.1.1",
|
|
65
|
+
nypm: "^0.5.4",
|
|
45
66
|
open: "^10.1.0",
|
|
67
|
+
"std-env": "^3.9.0",
|
|
46
68
|
"yocto-spinner": "^1.0.0",
|
|
47
69
|
zod: "^4.0.5"
|
|
48
70
|
},
|
|
49
71
|
devDependencies: {
|
|
50
72
|
"@bigcommerce/eslint-config": "^2.11.0",
|
|
51
73
|
"@bigcommerce/eslint-config-catalyst": "workspace:^",
|
|
52
|
-
"@commander-js/extra-typings": "^14.0.0",
|
|
53
74
|
"@types/adm-zip": "^0.5.7",
|
|
75
|
+
"@types/cross-spawn": "^6.0.6",
|
|
76
|
+
"@types/fs-extra": "^11.0.4",
|
|
77
|
+
"@types/lodash.kebabcase": "^4.1.9",
|
|
54
78
|
"@types/node": "^22.15.30",
|
|
55
79
|
"@vitest/coverage-v8": "^3.2.4",
|
|
56
80
|
"@vitest/ui": "^3.2.4",
|
|
@@ -66,23 +90,15 @@ var package_default = {
|
|
|
66
90
|
}
|
|
67
91
|
};
|
|
68
92
|
|
|
69
|
-
// src/cli/lib/
|
|
93
|
+
// src/cli/lib/user-config.ts
|
|
70
94
|
import Conf from "conf";
|
|
71
|
-
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
var userConfigInstance;
|
|
96
|
+
function getUserConfig() {
|
|
97
|
+
userConfigInstance ?? (userConfigInstance = new Conf({
|
|
98
|
+
projectName: "catalyst-cli",
|
|
75
99
|
projectSuffix: "",
|
|
76
|
-
configName: "
|
|
100
|
+
configName: "config",
|
|
77
101
|
schema: {
|
|
78
|
-
projectUuid: { type: "string", format: "uuid" },
|
|
79
|
-
framework: {
|
|
80
|
-
type: "string",
|
|
81
|
-
enum: ["catalyst"],
|
|
82
|
-
default: "catalyst"
|
|
83
|
-
},
|
|
84
|
-
storeHash: { type: "string" },
|
|
85
|
-
accessToken: { type: "string" },
|
|
86
102
|
telemetry: {
|
|
87
103
|
type: "object",
|
|
88
104
|
properties: {
|
|
@@ -91,7 +107,8 @@ function getProjectConfig() {
|
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
|
-
});
|
|
110
|
+
}));
|
|
111
|
+
return userConfigInstance;
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
// src/cli/lib/telemetry.ts
|
|
@@ -104,10 +121,10 @@ var Telemetry = class {
|
|
|
104
121
|
this.projectVersion = package_default.version;
|
|
105
122
|
this.setEnabled = (_enabled) => {
|
|
106
123
|
const enabled = Boolean(_enabled);
|
|
107
|
-
this.
|
|
124
|
+
this.userConfig.set("telemetry.enabled", enabled);
|
|
108
125
|
};
|
|
109
126
|
this.CATALYST_TELEMETRY_DISABLED = process.env.CATALYST_TELEMETRY_DISABLED;
|
|
110
|
-
this.
|
|
127
|
+
this.userConfig = getUserConfig();
|
|
111
128
|
this.correlationId = randomUUID();
|
|
112
129
|
this.startTime = Date.now();
|
|
113
130
|
this.analytics = new Analytics({
|
|
@@ -159,15 +176,15 @@ var Telemetry = class {
|
|
|
159
176
|
});
|
|
160
177
|
}
|
|
161
178
|
isEnabled() {
|
|
162
|
-
return !this.CATALYST_TELEMETRY_DISABLED && this.
|
|
179
|
+
return !this.CATALYST_TELEMETRY_DISABLED && this.userConfig.get(TELEMETRY_KEY_ENABLED, true);
|
|
163
180
|
}
|
|
164
181
|
getAnonymousId() {
|
|
165
|
-
const val = this.
|
|
182
|
+
const val = this.userConfig.get(TELEMETRY_KEY_ID);
|
|
166
183
|
if (val) {
|
|
167
184
|
return val;
|
|
168
185
|
}
|
|
169
186
|
const generated = randomBytes(32).toString("hex");
|
|
170
|
-
this.
|
|
187
|
+
this.userConfig.set(TELEMETRY_KEY_ID, generated);
|
|
171
188
|
return generated;
|
|
172
189
|
}
|
|
173
190
|
};
|
|
@@ -178,27 +195,33 @@ function getTelemetry() {
|
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
// src/cli/program.ts
|
|
181
|
-
import { Option as
|
|
182
|
-
import { Command as
|
|
183
|
-
import { colorize as
|
|
198
|
+
import { Option as Option9 } from "@commander-js/extra-typings";
|
|
199
|
+
import { Command as Command12 } from "commander";
|
|
200
|
+
import { colorize as colorize8 } from "consola/utils";
|
|
184
201
|
import { config } from "dotenv";
|
|
202
|
+
import { existsSync as existsSync5 } from "fs";
|
|
185
203
|
import { resolve } from "path";
|
|
186
204
|
|
|
187
205
|
// src/cli/commands/auth.ts
|
|
188
|
-
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 { password } from "@inquirer/prompts";
|
|
189
211
|
import { colorize } from "consola/utils";
|
|
190
212
|
import open from "open";
|
|
191
213
|
import yoctoSpinner from "yocto-spinner";
|
|
192
|
-
import { z as
|
|
214
|
+
import { z as z2 } from "zod";
|
|
193
215
|
|
|
194
216
|
// src/cli/lib/auth.ts
|
|
195
217
|
import { z } from "zod";
|
|
196
|
-
var DEVICE_OAUTH_CLIENT_ID = "
|
|
218
|
+
var DEVICE_OAUTH_CLIENT_ID = "b8063bu6hhml4e0lqh22yut63atsbyv";
|
|
197
219
|
var DEVICE_OAUTH_SCOPES = [
|
|
198
220
|
"store_v2_information",
|
|
199
221
|
"store_infrastructure_deployments_manage",
|
|
200
222
|
"store_infrastructure_logs_read_only",
|
|
201
|
-
"store_infrastructure_projects_manage"
|
|
223
|
+
"store_infrastructure_projects_manage",
|
|
224
|
+
"store_channel_settings"
|
|
202
225
|
].join(" ");
|
|
203
226
|
var DEFAULT_LOGIN_URL = "https://login.bigcommerce.com";
|
|
204
227
|
var DeviceCodeResponseSchema = z.object({
|
|
@@ -255,27 +278,149 @@ async function waitForDeviceToken(loginUrl, deviceCode, interval) {
|
|
|
255
278
|
return waitForDeviceToken(loginUrl, deviceCode, interval);
|
|
256
279
|
}
|
|
257
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 consola.prompt("Store hash:", { type: "text" });
|
|
341
|
+
const storeHash = String(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 consola.prompt(
|
|
370
|
+
"Try logging in manually with a store hash and access token instead?",
|
|
371
|
+
{ type: "confirm", initial: true }
|
|
372
|
+
);
|
|
373
|
+
if (!shouldFallback) {
|
|
374
|
+
throw new LoginAbortedError();
|
|
375
|
+
}
|
|
376
|
+
return manualLogin(apiHost);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
258
380
|
// src/cli/lib/project.ts
|
|
259
|
-
import { z as
|
|
260
|
-
var
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
381
|
+
import { z as z3 } from "zod";
|
|
382
|
+
var InfrastructureProjectValidationError = class extends Error {
|
|
383
|
+
constructor(message) {
|
|
384
|
+
super(message);
|
|
385
|
+
this.name = "InfrastructureProjectValidationError";
|
|
386
|
+
}
|
|
387
|
+
};
|
|
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())
|
|
265
394
|
})
|
|
266
395
|
)
|
|
267
396
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
397
|
+
function projectsUrl(storeHash, apiHost) {
|
|
398
|
+
return `https://${apiHost}/stores/${storeHash}/v3/infrastructure/projects`;
|
|
399
|
+
}
|
|
400
|
+
function authHeaders(accessToken) {
|
|
401
|
+
return {
|
|
402
|
+
"X-Auth-Token": accessToken,
|
|
403
|
+
"X-Correlation-Id": getTelemetry().correlationId
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function hasProjectsAccess(storeHash, accessToken, apiHost) {
|
|
407
|
+
const response = await fetch(projectsUrl(storeHash, apiHost), {
|
|
408
|
+
method: "GET",
|
|
409
|
+
headers: authHeaders(accessToken)
|
|
410
|
+
});
|
|
411
|
+
assertAuthorized(response);
|
|
412
|
+
if (response.status === 200) return true;
|
|
413
|
+
if (response.status === 403) return false;
|
|
414
|
+
throw new Error(
|
|
415
|
+
`GET /v3/infrastructure/projects failed: ${response.status} ${response.statusText}`
|
|
278
416
|
);
|
|
417
|
+
}
|
|
418
|
+
async function fetchProjects(storeHash, accessToken, apiHost) {
|
|
419
|
+
const response = await fetch(projectsUrl(storeHash, apiHost), {
|
|
420
|
+
method: "GET",
|
|
421
|
+
headers: authHeaders(accessToken)
|
|
422
|
+
});
|
|
423
|
+
assertAuthorized(response);
|
|
279
424
|
if (response.status === 403) {
|
|
280
425
|
throw new Error(
|
|
281
426
|
"Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
|
|
@@ -288,40 +433,52 @@ async function fetchProjects(storeHash, accessToken, apiHost) {
|
|
|
288
433
|
const { data } = fetchProjectsSchema.parse(res);
|
|
289
434
|
return data;
|
|
290
435
|
}
|
|
291
|
-
var createProjectSchema =
|
|
292
|
-
data:
|
|
293
|
-
uuid:
|
|
294
|
-
name:
|
|
295
|
-
date_created:
|
|
296
|
-
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()
|
|
297
442
|
})
|
|
298
443
|
});
|
|
444
|
+
var validationErrorBodySchema = z3.object({
|
|
445
|
+
title: z3.string().optional(),
|
|
446
|
+
detail: z3.string().optional(),
|
|
447
|
+
errors: z3.record(z3.string(), z3.string()).optional()
|
|
448
|
+
});
|
|
449
|
+
function extractValidationMessage(body) {
|
|
450
|
+
const parsed = validationErrorBodySchema.safeParse(body);
|
|
451
|
+
if (!parsed.success) return null;
|
|
452
|
+
const { title, detail, errors } = parsed.data;
|
|
453
|
+
if (errors && Object.keys(errors).length > 0) {
|
|
454
|
+
return Object.values(errors).join("; ");
|
|
455
|
+
}
|
|
456
|
+
return detail ?? title ?? null;
|
|
457
|
+
}
|
|
299
458
|
async function createProject(name, storeHash, accessToken, apiHost) {
|
|
300
|
-
const response = await fetch(
|
|
301
|
-
|
|
302
|
-
{
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
throw new
|
|
459
|
+
const response = await fetch(projectsUrl(storeHash, apiHost), {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: {
|
|
462
|
+
...authHeaders(accessToken),
|
|
463
|
+
Accept: "application/json",
|
|
464
|
+
"Content-Type": "application/json"
|
|
465
|
+
},
|
|
466
|
+
body: JSON.stringify({ name })
|
|
467
|
+
});
|
|
468
|
+
assertAuthorized(response);
|
|
469
|
+
if (response.status === 400 || response.status === 422) {
|
|
470
|
+
const body = await response.json().catch(() => null);
|
|
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;
|
|
472
|
+
const message = extractValidationMessage(body) ?? fallback;
|
|
473
|
+
throw new InfrastructureProjectValidationError(message);
|
|
315
474
|
}
|
|
316
475
|
if (response.status === 403) {
|
|
317
476
|
throw new Error(
|
|
318
477
|
"Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
|
|
319
478
|
);
|
|
320
479
|
}
|
|
321
|
-
if (response.status ===
|
|
322
|
-
throw new Error(
|
|
323
|
-
"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 (.)"
|
|
324
|
-
);
|
|
480
|
+
if (response.status === 502) {
|
|
481
|
+
throw new Error("Failed to create project, is the name already in use?");
|
|
325
482
|
}
|
|
326
483
|
if (!response.ok) {
|
|
327
484
|
throw new Error(`Failed to create project: ${response.statusText}`);
|
|
@@ -330,14 +487,85 @@ async function createProject(name, storeHash, accessToken, apiHost) {
|
|
|
330
487
|
const { data } = createProjectSchema.parse(res);
|
|
331
488
|
return data;
|
|
332
489
|
}
|
|
490
|
+
async function deleteProject(projectUuid, storeHash, accessToken, apiHost) {
|
|
491
|
+
const response = await fetch(`${projectsUrl(storeHash, apiHost)}/${projectUuid}`, {
|
|
492
|
+
method: "DELETE",
|
|
493
|
+
headers: authHeaders(accessToken)
|
|
494
|
+
});
|
|
495
|
+
assertAuthorized(response);
|
|
496
|
+
if (response.status === 403) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
"Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it."
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
if (response.status === 404) {
|
|
502
|
+
throw new Error(`Project ${projectUuid} not found.`);
|
|
503
|
+
}
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
throw new Error(`Failed to delete project: ${response.statusText}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/cli/lib/project-config.ts
|
|
510
|
+
import Conf2 from "conf";
|
|
511
|
+
import { join } from "path";
|
|
512
|
+
function getProjectConfig() {
|
|
513
|
+
return new Conf2({
|
|
514
|
+
cwd: join(process.cwd(), ".bigcommerce"),
|
|
515
|
+
projectSuffix: "",
|
|
516
|
+
configName: "project",
|
|
517
|
+
schema: {
|
|
518
|
+
projectUuid: { type: "string", format: "uuid" },
|
|
519
|
+
framework: {
|
|
520
|
+
type: "string",
|
|
521
|
+
enum: ["catalyst"],
|
|
522
|
+
default: "catalyst"
|
|
523
|
+
},
|
|
524
|
+
storeHash: { type: "string" },
|
|
525
|
+
accessToken: { type: "string" },
|
|
526
|
+
env: {
|
|
527
|
+
type: "object",
|
|
528
|
+
additionalProperties: { type: "string" },
|
|
529
|
+
default: {}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
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
|
+
};
|
|
333
561
|
|
|
334
562
|
// src/cli/commands/auth.ts
|
|
335
|
-
var
|
|
336
|
-
data:
|
|
337
|
-
store_name:
|
|
563
|
+
var StoreProfileSchema2 = z4.object({
|
|
564
|
+
data: z4.object({
|
|
565
|
+
store_name: z4.string()
|
|
338
566
|
})
|
|
339
567
|
});
|
|
340
|
-
async function
|
|
568
|
+
async function fetchStoreProfile2(storeHash, accessToken, apiHost) {
|
|
341
569
|
const response = await fetch(`https://${apiHost}/stores/${storeHash}/v3/settings/store/profile`, {
|
|
342
570
|
method: "GET",
|
|
343
571
|
headers: {
|
|
@@ -345,28 +573,36 @@ async function fetchStoreProfile(storeHash, accessToken, apiHost) {
|
|
|
345
573
|
Accept: "application/json"
|
|
346
574
|
}
|
|
347
575
|
});
|
|
576
|
+
assertAuthorized(response);
|
|
348
577
|
if (!response.ok) {
|
|
349
578
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
350
579
|
}
|
|
351
580
|
const res = await response.json();
|
|
352
|
-
const result =
|
|
581
|
+
const result = StoreProfileSchema2.safeParse(res);
|
|
353
582
|
if (!result.success) {
|
|
354
583
|
throw new Error("Unexpected response from store profile API");
|
|
355
584
|
}
|
|
356
585
|
return result.data.data;
|
|
357
586
|
}
|
|
358
|
-
var whoami = new Command("whoami").description("Verify stored credentials and display store/project info.").
|
|
359
|
-
|
|
587
|
+
var whoami = new Command("whoami").configureHelp({ showGlobalOptions: true }).description("Verify stored credentials and display store/project info.").addHelpText(
|
|
588
|
+
"after",
|
|
589
|
+
`
|
|
590
|
+
Example:
|
|
591
|
+
$ catalyst auth whoami
|
|
592
|
+
|
|
593
|
+
Logged in to My Store (abc123), connected to project my-project (43eba682-0c48-11f1-9bd5-827a48b0ce1e)`
|
|
594
|
+
).addOption(
|
|
595
|
+
new Option2(
|
|
360
596
|
"--store-hash <hash>",
|
|
361
597
|
"BigCommerce store hash. Can be found in the URL of your store Control Panel."
|
|
362
598
|
).env("CATALYST_STORE_HASH")
|
|
363
599
|
).addOption(
|
|
364
|
-
new
|
|
600
|
+
new Option2(
|
|
365
601
|
"--access-token <token>",
|
|
366
602
|
"BigCommerce access token. Can be found after creating a store-level API account."
|
|
367
603
|
).env("CATALYST_ACCESS_TOKEN")
|
|
368
604
|
).addOption(
|
|
369
|
-
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()
|
|
370
606
|
).action(async (options) => {
|
|
371
607
|
try {
|
|
372
608
|
const config2 = getProjectConfig();
|
|
@@ -380,7 +616,7 @@ var whoami = new Command("whoami").description("Verify stored credentials and di
|
|
|
380
616
|
process.exit(1);
|
|
381
617
|
return;
|
|
382
618
|
}
|
|
383
|
-
const store = await
|
|
619
|
+
const store = await fetchStoreProfile2(storeHash, accessToken, options.apiHost);
|
|
384
620
|
const projectUuid = config2.get("projectUuid");
|
|
385
621
|
if (projectUuid) {
|
|
386
622
|
const projects = await fetchProjects(storeHash, accessToken, options.apiHost);
|
|
@@ -400,7 +636,11 @@ var whoami = new Command("whoami").description("Verify stored credentials and di
|
|
|
400
636
|
process.exit(0);
|
|
401
637
|
} catch (error) {
|
|
402
638
|
const message = error instanceof Error ? error.message : String(error);
|
|
403
|
-
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")) {
|
|
404
644
|
consola.error(`Not logged in: invalid credentials (${message})`);
|
|
405
645
|
} else {
|
|
406
646
|
consola.error(`Failed to verify credentials: ${message}`);
|
|
@@ -408,19 +648,30 @@ var whoami = new Command("whoami").description("Verify stored credentials and di
|
|
|
408
648
|
process.exit(1);
|
|
409
649
|
}
|
|
410
650
|
});
|
|
411
|
-
var
|
|
412
|
-
|
|
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."
|
|
653
|
+
).addHelpText(
|
|
654
|
+
"after",
|
|
655
|
+
`
|
|
656
|
+
Examples:
|
|
657
|
+
# Login interactively (browser, with manual fallback)
|
|
658
|
+
$ catalyst auth login
|
|
659
|
+
|
|
660
|
+
# Login with existing credentials (skips interactive flow)
|
|
661
|
+
$ catalyst auth login --store-hash <STORE_HASH> --access-token <ACCESS_TOKEN>`
|
|
662
|
+
).addOption(
|
|
663
|
+
new Option2(
|
|
413
664
|
"--store-hash <hash>",
|
|
414
665
|
"BigCommerce store hash. Can be found in the URL of your store Control Panel."
|
|
415
666
|
).env("CATALYST_STORE_HASH")
|
|
416
667
|
).addOption(
|
|
417
|
-
new
|
|
668
|
+
new Option2(
|
|
418
669
|
"--access-token <token>",
|
|
419
670
|
"BigCommerce access token. Can be found after creating a store-level API account."
|
|
420
671
|
).env("CATALYST_ACCESS_TOKEN")
|
|
421
672
|
).addOption(
|
|
422
|
-
new
|
|
423
|
-
).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) => {
|
|
424
675
|
try {
|
|
425
676
|
const config2 = getProjectConfig();
|
|
426
677
|
const storeHash = options.storeHash ?? config2.get("storeHash");
|
|
@@ -431,36 +682,30 @@ var login = new Command("login").description("Authenticate via browser using the
|
|
|
431
682
|
process.exit(0);
|
|
432
683
|
return;
|
|
433
684
|
}
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
} 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) {
|
|
442
692
|
consola.info(
|
|
443
|
-
|
|
693
|
+
"Login aborted. Re-run `catalyst auth login` when you have your credentials ready."
|
|
444
694
|
);
|
|
695
|
+
process.exit(0);
|
|
696
|
+
return;
|
|
445
697
|
}
|
|
446
|
-
const spinner = yoctoSpinner().start("Waiting for authentication...");
|
|
447
|
-
const credentials = await waitForDeviceToken(
|
|
448
|
-
options.loginUrl,
|
|
449
|
-
deviceCode.device_code,
|
|
450
|
-
deviceCode.interval
|
|
451
|
-
);
|
|
452
|
-
spinner.success("Authentication complete.");
|
|
453
|
-
config2.set("storeHash", credentials.store_hash);
|
|
454
|
-
config2.set("accessToken", credentials.access_token);
|
|
455
|
-
consola.success(`Logged in to store ${credentials.store_hash}.`);
|
|
456
|
-
process.exit(0);
|
|
457
|
-
} catch (error) {
|
|
458
698
|
const message = error instanceof Error ? error.message : String(error);
|
|
459
699
|
consola.error(`Login failed: ${message}`);
|
|
460
700
|
process.exit(1);
|
|
461
701
|
}
|
|
462
702
|
});
|
|
463
|
-
var logout = new Command("logout").description("Remove stored credentials for the current project.").
|
|
703
|
+
var logout = new Command("logout").configureHelp({ showGlobalOptions: true }).description("Remove stored credentials for the current project.").addHelpText(
|
|
704
|
+
"after",
|
|
705
|
+
`
|
|
706
|
+
Example:
|
|
707
|
+
$ catalyst auth logout`
|
|
708
|
+
).action(() => {
|
|
464
709
|
try {
|
|
465
710
|
const config2 = getProjectConfig();
|
|
466
711
|
const storeHash = config2.get("storeHash");
|
|
@@ -480,13 +725,13 @@ var logout = new Command("logout").description("Remove stored credentials for th
|
|
|
480
725
|
process.exit(1);
|
|
481
726
|
}
|
|
482
727
|
});
|
|
483
|
-
var auth = new Command("auth").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);
|
|
484
729
|
|
|
485
730
|
// src/cli/commands/build.ts
|
|
486
|
-
import { Command as Command2, Option as
|
|
731
|
+
import { Command as Command2, Option as Option3 } from "commander";
|
|
487
732
|
import { execa } from "execa";
|
|
488
|
-
import { copyFile, cp, writeFile } from "fs/promises";
|
|
489
|
-
import { join as
|
|
733
|
+
import { copyFile, cp, rm, writeFile } from "fs/promises";
|
|
734
|
+
import { join as join4 } from "path";
|
|
490
735
|
|
|
491
736
|
// src/cli/lib/get-module-cli-path.ts
|
|
492
737
|
import { dirname, join as join2 } from "path";
|
|
@@ -497,6 +742,46 @@ function getModuleCliPath() {
|
|
|
497
742
|
return join2(__dirname, "..");
|
|
498
743
|
}
|
|
499
744
|
|
|
745
|
+
// src/cli/lib/project-state.ts
|
|
746
|
+
import { existsSync, readFileSync } from "fs";
|
|
747
|
+
import { join as join3 } from "path";
|
|
748
|
+
import { z as z5 } from "zod";
|
|
749
|
+
var projectJsonSchema = z5.looseObject({
|
|
750
|
+
projectUuid: z5.string().optional()
|
|
751
|
+
});
|
|
752
|
+
var packageJsonSchema = z5.looseObject({
|
|
753
|
+
dependencies: z5.record(z5.string(), z5.string()).optional()
|
|
754
|
+
});
|
|
755
|
+
var safeReadJson = (path) => {
|
|
756
|
+
try {
|
|
757
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
function getProjectState(cwd = process.cwd()) {
|
|
763
|
+
const projectJson = projectJsonSchema.safeParse(
|
|
764
|
+
safeReadJson(join3(cwd, ".bigcommerce", "project.json"))
|
|
765
|
+
);
|
|
766
|
+
const projectUuid = projectJson.success ? projectJson.data.projectUuid : void 0;
|
|
767
|
+
const hasMiddleware = existsSync(join3(cwd, "middleware.ts"));
|
|
768
|
+
const hasProxy = existsSync(join3(cwd, "proxy.ts"));
|
|
769
|
+
const pkgJson = packageJsonSchema.safeParse(safeReadJson(join3(cwd, "package.json")));
|
|
770
|
+
const hasOpenNextDep = pkgJson.success ? Boolean(pkgJson.data.dependencies?.["@opennextjs/cloudflare"]) : false;
|
|
771
|
+
const isLinked = Boolean(projectUuid);
|
|
772
|
+
const isTransformed = hasMiddleware && !hasProxy && hasOpenNextDep;
|
|
773
|
+
const isFullySetUp = isLinked && isTransformed;
|
|
774
|
+
return {
|
|
775
|
+
projectUuid,
|
|
776
|
+
hasMiddleware,
|
|
777
|
+
hasProxy,
|
|
778
|
+
hasOpenNextDep,
|
|
779
|
+
isLinked,
|
|
780
|
+
isTransformed,
|
|
781
|
+
isFullySetUp
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
500
785
|
// src/cli/lib/wrangler-config.ts
|
|
501
786
|
function getWranglerConfig(projectUuid) {
|
|
502
787
|
return {
|
|
@@ -556,19 +841,20 @@ function getWranglerConfig(projectUuid) {
|
|
|
556
841
|
}
|
|
557
842
|
|
|
558
843
|
// src/cli/commands/build.ts
|
|
559
|
-
var WRANGLER_VERSION = "4.
|
|
844
|
+
var WRANGLER_VERSION = "4.90.0";
|
|
560
845
|
async function buildCatalystProject(projectUuid) {
|
|
561
846
|
const coreDir = process.cwd();
|
|
562
|
-
const openNextOutDir =
|
|
563
|
-
const bigcommerceDistDir =
|
|
847
|
+
const openNextOutDir = join4(coreDir, ".open-next");
|
|
848
|
+
const bigcommerceDistDir = join4(coreDir, ".bigcommerce", "dist");
|
|
564
849
|
const wranglerConfig = getWranglerConfig(projectUuid);
|
|
850
|
+
await rm(bigcommerceDistDir, { recursive: true, force: true });
|
|
565
851
|
consola.start("Copying templates...");
|
|
566
852
|
await copyFile(
|
|
567
|
-
|
|
568
|
-
|
|
853
|
+
join4(getModuleCliPath(), "templates", "open-next.config.ts"),
|
|
854
|
+
join4(coreDir, ".bigcommerce", "open-next.config.ts")
|
|
569
855
|
);
|
|
570
856
|
await writeFile(
|
|
571
|
-
|
|
857
|
+
join4(coreDir, ".bigcommerce", "wrangler.jsonc"),
|
|
572
858
|
JSON.stringify(wranglerConfig, null, 2)
|
|
573
859
|
);
|
|
574
860
|
consola.success("Templates copied");
|
|
@@ -581,7 +867,7 @@ async function buildCatalystProject(projectUuid) {
|
|
|
581
867
|
"build",
|
|
582
868
|
"--skipWranglerConfigCheck",
|
|
583
869
|
"--openNextConfigPath",
|
|
584
|
-
|
|
870
|
+
join4(coreDir, ".bigcommerce", "open-next.config.ts")
|
|
585
871
|
],
|
|
586
872
|
{
|
|
587
873
|
stdout: ["pipe", "inherit"],
|
|
@@ -595,7 +881,7 @@ async function buildCatalystProject(projectUuid) {
|
|
|
595
881
|
`wrangler@${WRANGLER_VERSION}`,
|
|
596
882
|
"deploy",
|
|
597
883
|
"--config",
|
|
598
|
-
|
|
884
|
+
join4(coreDir, ".bigcommerce", "wrangler.jsonc"),
|
|
599
885
|
"--keep-vars",
|
|
600
886
|
"--outdir",
|
|
601
887
|
bigcommerceDistDir,
|
|
@@ -607,17 +893,37 @@ async function buildCatalystProject(projectUuid) {
|
|
|
607
893
|
}
|
|
608
894
|
);
|
|
609
895
|
consola.success("Project built");
|
|
610
|
-
await cp(
|
|
896
|
+
await cp(join4(openNextOutDir, "assets"), join4(bigcommerceDistDir, "assets"), {
|
|
611
897
|
recursive: true,
|
|
612
898
|
force: true
|
|
613
899
|
});
|
|
614
900
|
}
|
|
615
|
-
var build = new Command2("build").
|
|
616
|
-
|
|
901
|
+
var build = new Command2("build").configureHelp({ showGlobalOptions: true }).description(
|
|
902
|
+
"Build your Catalyst project using the OpenNext/Cloudflare build pipeline. Also runs a Wrangler dry-run to generate deployment artifacts."
|
|
903
|
+
).addHelpText(
|
|
904
|
+
"after",
|
|
905
|
+
`
|
|
906
|
+
Examples:
|
|
907
|
+
$ catalyst build
|
|
908
|
+
|
|
909
|
+
# Include project UUID
|
|
910
|
+
$ catalyst build --project-uuid <UUID>`
|
|
911
|
+
).addOption(
|
|
912
|
+
new Option3(
|
|
617
913
|
"--project-uuid <uuid>",
|
|
618
914
|
"Project UUID to be included in the deployment configuration."
|
|
619
915
|
).env("CATALYST_PROJECT_UUID")
|
|
620
916
|
).action(async (options) => {
|
|
917
|
+
const state = getProjectState();
|
|
918
|
+
if (!state.isTransformed) {
|
|
919
|
+
consola.info("Project is not set up for Commerce Hosting \u2014 running `next build`.");
|
|
920
|
+
consola.info("To deploy to Commerce Hosting, run `catalyst deploy`.");
|
|
921
|
+
await execa("pnpm", ["exec", "next", "build"], {
|
|
922
|
+
stdio: "inherit",
|
|
923
|
+
cwd: process.cwd()
|
|
924
|
+
});
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
621
927
|
const config2 = getProjectConfig();
|
|
622
928
|
const projectUuid = options.projectUuid ?? config2.get("projectUuid");
|
|
623
929
|
if (!projectUuid) {
|
|
@@ -628,34 +934,563 @@ var build = new Command2("build").addOption(
|
|
|
628
934
|
await buildCatalystProject(projectUuid);
|
|
629
935
|
});
|
|
630
936
|
|
|
631
|
-
// src/cli/commands/
|
|
632
|
-
import
|
|
633
|
-
import { Command as Command3, Option as Option3 } from "commander";
|
|
634
|
-
import { colorize as colorize2 } from "consola/utils";
|
|
635
|
-
import { access, readdir, readFile } from "fs/promises";
|
|
636
|
-
import { join as join4 } from "path";
|
|
637
|
-
import yoctoSpinner2 from "yocto-spinner";
|
|
638
|
-
import { z as z4 } from "zod";
|
|
937
|
+
// src/cli/commands/channel.ts
|
|
938
|
+
import { Command as Command3, Option as Option4 } from "commander";
|
|
639
939
|
|
|
640
|
-
// src/cli/lib/
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
- A worker.js file larger than 40 MB
|
|
647
|
-
- An individual asset file larger than 25 MB
|
|
648
|
-
- More than 1,000 total files in the bundle`,
|
|
649
|
-
50: "Deployment failed. This is usually a temporary issue \u2014 please try again. If the problem persists, contact support.",
|
|
650
|
-
60: "Your code was deployed, but we couldn't determine your deployment URL. Please try deploying again. If the issue persists, contact support."
|
|
940
|
+
// src/cli/lib/channels.ts
|
|
941
|
+
import { z as z6 } from "zod";
|
|
942
|
+
var cliApiUrl = (origin, storeHash, path) => `${origin}/stores/${storeHash}/cli-api/v3${path}`;
|
|
943
|
+
var channelsUrl = (storeHash, apiHost, query2 = {}) => {
|
|
944
|
+
const params = new URLSearchParams(query2).toString();
|
|
945
|
+
return `https://${apiHost}/stores/${storeHash}/v3/channels${params ? `?${params}` : ""}`;
|
|
651
946
|
};
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
947
|
+
var authHeaders2 = (accessToken) => ({
|
|
948
|
+
"X-Auth-Token": accessToken,
|
|
949
|
+
"X-Correlation-Id": getTelemetry().correlationId,
|
|
950
|
+
Accept: "application/json"
|
|
951
|
+
});
|
|
952
|
+
var envVarsSchema = z6.record(z6.string(), z6.coerce.string());
|
|
953
|
+
var channelSchema = z6.object({
|
|
954
|
+
id: z6.number(),
|
|
955
|
+
name: z6.string(),
|
|
956
|
+
platform: z6.string()
|
|
957
|
+
});
|
|
958
|
+
var channelsResponseSchema = z6.object({
|
|
959
|
+
data: z6.array(channelSchema)
|
|
960
|
+
});
|
|
961
|
+
var initResponseSchema = z6.object({
|
|
962
|
+
data: z6.object({
|
|
963
|
+
storefront_api_token: z6.string(),
|
|
964
|
+
envVars: envVarsSchema
|
|
965
|
+
})
|
|
966
|
+
});
|
|
967
|
+
var createChannelResponseSchema = z6.object({
|
|
968
|
+
data: z6.object({
|
|
969
|
+
id: z6.number(),
|
|
970
|
+
storefront_api_token: z6.string(),
|
|
971
|
+
envVars: envVarsSchema
|
|
972
|
+
})
|
|
973
|
+
});
|
|
974
|
+
var eligibilityResponseSchema = z6.object({
|
|
975
|
+
data: z6.object({
|
|
976
|
+
eligible: z6.boolean(),
|
|
977
|
+
message: z6.string()
|
|
978
|
+
})
|
|
979
|
+
});
|
|
980
|
+
var channelSiteSchema = z6.object({
|
|
981
|
+
data: z6.object({
|
|
982
|
+
id: z6.number(),
|
|
983
|
+
url: z6.string(),
|
|
984
|
+
channel_id: z6.number()
|
|
985
|
+
})
|
|
986
|
+
});
|
|
987
|
+
async function getChannelInit(channelId, storeHash, accessToken, origin) {
|
|
988
|
+
const response = await fetch(cliApiUrl(origin, storeHash, `/channels/${channelId}/init`), {
|
|
989
|
+
method: "GET",
|
|
990
|
+
headers: authHeaders2(accessToken)
|
|
991
|
+
});
|
|
992
|
+
if (!response.ok) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
`GET /channels/${channelId}/init failed: ${response.status} ${response.statusText}`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const { data } = initResponseSchema.parse(await response.json());
|
|
998
|
+
return { storefrontToken: data.storefront_api_token, envVars: data.envVars };
|
|
999
|
+
}
|
|
1000
|
+
async function checkChannelEligibility(storeHash, accessToken, origin) {
|
|
1001
|
+
const response = await fetch(cliApiUrl(origin, storeHash, "/channels/catalyst/eligibility"), {
|
|
1002
|
+
method: "GET",
|
|
1003
|
+
headers: authHeaders2(accessToken)
|
|
1004
|
+
});
|
|
1005
|
+
if (!response.ok) {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`GET /channels/catalyst/eligibility failed: ${response.status} ${response.statusText}`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
return eligibilityResponseSchema.parse(await response.json()).data;
|
|
1011
|
+
}
|
|
1012
|
+
async function createChannel(name, storefrontLocale, additionalLocales, installSampleData, storeHash, accessToken, origin) {
|
|
1013
|
+
const response = await fetch(cliApiUrl(origin, storeHash, "/channels/catalyst"), {
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
headers: { ...authHeaders2(accessToken), "Content-Type": "application/json" },
|
|
1016
|
+
body: JSON.stringify({
|
|
1017
|
+
name,
|
|
1018
|
+
initialData: { type: installSampleData ? "sample" : "none" },
|
|
1019
|
+
deployStorefront: true,
|
|
1020
|
+
devOrigin: "http://localhost:3000",
|
|
1021
|
+
storefrontLanguage: storefrontLocale,
|
|
1022
|
+
additionalLocales
|
|
1023
|
+
})
|
|
1024
|
+
});
|
|
1025
|
+
if (!response.ok) {
|
|
1026
|
+
throw new Error(`POST /channels/catalyst failed: ${response.status} ${response.statusText}`);
|
|
1027
|
+
}
|
|
1028
|
+
const { data } = createChannelResponseSchema.parse(await response.json());
|
|
1029
|
+
return {
|
|
1030
|
+
channelId: data.id,
|
|
1031
|
+
storefrontToken: data.storefront_api_token,
|
|
1032
|
+
envVars: data.envVars
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
async function fetchAvailableChannels(storeHash, accessToken, apiHost) {
|
|
1036
|
+
const response = await fetch(
|
|
1037
|
+
channelsUrl(storeHash, apiHost, { available: "true", type: "storefront" }),
|
|
1038
|
+
{
|
|
1039
|
+
method: "GET",
|
|
1040
|
+
headers: {
|
|
1041
|
+
"X-Auth-Token": accessToken,
|
|
1042
|
+
"X-Correlation-Id": getTelemetry().correlationId,
|
|
1043
|
+
Accept: "application/json"
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
);
|
|
1047
|
+
assertAuthorized(response);
|
|
1048
|
+
if (!response.ok) {
|
|
1049
|
+
throw new Error(`GET /v3/channels failed: ${response.status} ${response.statusText}`);
|
|
1050
|
+
}
|
|
1051
|
+
return channelsResponseSchema.parse(await response.json()).data;
|
|
1052
|
+
}
|
|
1053
|
+
async function updateChannelSiteUrl(channelId, siteUrl, storeHash, accessToken, apiHost) {
|
|
1054
|
+
const response = await fetch(
|
|
1055
|
+
`https://${apiHost}/stores/${storeHash}/v3/channels/${channelId}/site`,
|
|
1056
|
+
{
|
|
1057
|
+
method: "PUT",
|
|
1058
|
+
headers: {
|
|
1059
|
+
"X-Auth-Token": accessToken,
|
|
1060
|
+
"Content-Type": "application/json",
|
|
1061
|
+
Accept: "application/json",
|
|
1062
|
+
"X-Correlation-Id": getTelemetry().correlationId
|
|
1063
|
+
},
|
|
1064
|
+
body: JSON.stringify({ url: siteUrl })
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
if (response.status === 401 || response.status === 403) {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
`Failed to update channel site (${response.status}). Re-run \`catalyst auth login\` to refresh your access token with the store_channel_settings scope.`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (!response.ok) {
|
|
1073
|
+
throw new Error(`Failed to update channel site: ${response.status} ${response.statusText}`);
|
|
1074
|
+
}
|
|
1075
|
+
const res = await response.json();
|
|
1076
|
+
const { data } = channelSiteSchema.parse(res);
|
|
1077
|
+
return { id: data.id, url: data.url, channelId: data.channel_id };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/cli/lib/commerce-hosting.ts
|
|
1081
|
+
import { input, select, Separator } from "@inquirer/prompts";
|
|
1082
|
+
import { colorize as colorize2 } from "consola/utils";
|
|
1083
|
+
import {
|
|
1084
|
+
existsSync as existsSync2,
|
|
1085
|
+
lstatSync,
|
|
1086
|
+
mkdirSync,
|
|
1087
|
+
readFileSync as readFileSync2,
|
|
1088
|
+
symlinkSync,
|
|
1089
|
+
unlinkSync,
|
|
1090
|
+
writeFileSync
|
|
1091
|
+
} from "fs";
|
|
1092
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1093
|
+
import { z as z7 } from "zod";
|
|
1094
|
+
|
|
1095
|
+
// src/cli/lib/sort-package-json.ts
|
|
1096
|
+
var FIELD_ORDER = [
|
|
1097
|
+
"name",
|
|
1098
|
+
"description",
|
|
1099
|
+
"version",
|
|
1100
|
+
"private",
|
|
1101
|
+
"engines",
|
|
1102
|
+
"scripts",
|
|
1103
|
+
"dependencies",
|
|
1104
|
+
"devDependencies"
|
|
1105
|
+
];
|
|
1106
|
+
function sortPackageJsonFields(pkg) {
|
|
1107
|
+
const ordered = {};
|
|
1108
|
+
FIELD_ORDER.forEach((field) => {
|
|
1109
|
+
if (field in pkg) {
|
|
1110
|
+
ordered[field] = pkg[field];
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
Object.keys(pkg).forEach((key) => {
|
|
1114
|
+
if (!(key in ordered)) {
|
|
1115
|
+
ordered[key] = pkg[key];
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
return ordered;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/cli/lib/commerce-hosting.ts
|
|
1122
|
+
var OPENNEXT_CLOUDFLARE_VERSION = "1.17.3";
|
|
1123
|
+
var corePackageJsonSchema = z7.looseObject({
|
|
1124
|
+
dependencies: z7.record(z7.string(), z7.string()).optional()
|
|
1125
|
+
});
|
|
1126
|
+
var writeJson = (path, value) => {
|
|
1127
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
1128
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
1129
|
+
`);
|
|
1130
|
+
};
|
|
1131
|
+
var symlinkRootEnvToCore = (projectDir) => {
|
|
1132
|
+
const coreEnvPath = join5(projectDir, "core", ".env.local");
|
|
1133
|
+
if (lstatSync(coreEnvPath, { throwIfNoEntry: false })) return;
|
|
1134
|
+
try {
|
|
1135
|
+
symlinkSync("../.env.local", coreEnvPath);
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1138
|
+
consola.warn(
|
|
1139
|
+
`Could not create symlink at core/.env.local: ${message}
|
|
1140
|
+
On Windows, enable Developer Mode or run as administrator to allow symlinks.
|
|
1141
|
+
You will need to keep .env.local and core/.env.local in sync manually.`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
var convertProxyToMiddleware = (projectDir) => {
|
|
1146
|
+
const proxyPath = join5(projectDir, "core", "proxy.ts");
|
|
1147
|
+
const middlewarePath = join5(projectDir, "core", "middleware.ts");
|
|
1148
|
+
if (!existsSync2(proxyPath)) return;
|
|
1149
|
+
const contents = readFileSync2(proxyPath, "utf-8").replace("export const proxy", "export const middleware").replace("export const config = {", "export const config = {\n runtime: 'experimental-edge',");
|
|
1150
|
+
writeFileSync(middlewarePath, contents);
|
|
1151
|
+
unlinkSync(proxyPath);
|
|
1152
|
+
};
|
|
1153
|
+
var cleanupCloudflareIncompatibilities = async (projectDir) => {
|
|
1154
|
+
const instrumentationPath = join5(projectDir, "core", "instrumentation.ts");
|
|
1155
|
+
if (!existsSync2(instrumentationPath)) return;
|
|
1156
|
+
if (!process.stdin.isTTY) {
|
|
1157
|
+
consola.warn(
|
|
1158
|
+
"core/instrumentation.ts is present and may be incompatible with the Cloudflare Workers bundle (the default @vercel/otel scaffolding throws at cold start under workerd). Skipping automatic cleanup in non-interactive mode \u2014 re-run interactively to remove it, or delete/gate it manually."
|
|
1159
|
+
);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const shouldRemove = await consola.prompt(
|
|
1163
|
+
'Catalyst found core/instrumentation.ts, which is incompatible with the Cloudflare Workers bundle when it uses @vercel/otel (causes "Failed to prepare server" at cold start). Remove it and drop @vercel/otel from core/package.json?',
|
|
1164
|
+
{ type: "confirm", initial: true }
|
|
1165
|
+
);
|
|
1166
|
+
consola.log("");
|
|
1167
|
+
if (!shouldRemove) {
|
|
1168
|
+
consola.info(
|
|
1169
|
+
'Leaving core/instrumentation.ts in place. The Cloudflare worker will continue to log "Failed to prepare server" at cold start until this is resolved manually.'
|
|
1170
|
+
);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
unlinkSync(instrumentationPath);
|
|
1174
|
+
consola.info("Removed core/instrumentation.ts (incompatible with Cloudflare Workers).");
|
|
1175
|
+
const corePackageJsonPath = join5(projectDir, "core", "package.json");
|
|
1176
|
+
if (existsSync2(corePackageJsonPath)) {
|
|
1177
|
+
const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
|
|
1178
|
+
if (pkg.dependencies && "@vercel/otel" in pkg.dependencies) {
|
|
1179
|
+
const { "@vercel/otel": _removedVercelOtel, ...preservedDeps } = pkg.dependencies;
|
|
1180
|
+
pkg.dependencies = preservedDeps;
|
|
1181
|
+
writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
|
|
1182
|
+
consola.info("Dropped @vercel/otel from core/package.json.");
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
var setupCommerceHosting = async ({
|
|
1187
|
+
projectDir,
|
|
1188
|
+
projectUuid,
|
|
1189
|
+
storeHash,
|
|
1190
|
+
accessToken
|
|
1191
|
+
}) => {
|
|
1192
|
+
await cleanupCloudflareIncompatibilities(projectDir);
|
|
1193
|
+
const corePackageJsonPath = join5(projectDir, "core", "package.json");
|
|
1194
|
+
const pkg = corePackageJsonSchema.parse(JSON.parse(readFileSync2(corePackageJsonPath, "utf-8")));
|
|
1195
|
+
pkg.dependencies = {
|
|
1196
|
+
...pkg.dependencies,
|
|
1197
|
+
"@opennextjs/cloudflare": OPENNEXT_CLOUDFLARE_VERSION
|
|
1198
|
+
};
|
|
1199
|
+
writeJson(corePackageJsonPath, sortPackageJsonFields(pkg));
|
|
1200
|
+
const projectJson = {
|
|
1201
|
+
projectUuid,
|
|
1202
|
+
framework: "catalyst"
|
|
1203
|
+
};
|
|
1204
|
+
if (storeHash) projectJson.storeHash = storeHash;
|
|
1205
|
+
if (accessToken) projectJson.accessToken = accessToken;
|
|
1206
|
+
writeJson(join5(projectDir, "core", ".bigcommerce", "project.json"), projectJson);
|
|
1207
|
+
symlinkRootEnvToCore(projectDir);
|
|
1208
|
+
convertProxyToMiddleware(projectDir);
|
|
1209
|
+
};
|
|
1210
|
+
var NoLinkedProjectError = class extends Error {
|
|
1211
|
+
constructor() {
|
|
1212
|
+
super("No infrastructure project linked: user declined to create one.");
|
|
1213
|
+
this.name = "NoLinkedProjectError";
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
async function promptForNewProjectName(api) {
|
|
1217
|
+
const newProjectName = await consola.prompt("Enter a name for the new project:", {
|
|
1218
|
+
type: "text"
|
|
1219
|
+
});
|
|
1220
|
+
const data = await createProject(
|
|
1221
|
+
String(newProjectName),
|
|
1222
|
+
api.storeHash,
|
|
1223
|
+
api.accessToken,
|
|
1224
|
+
api.apiHost
|
|
1225
|
+
);
|
|
1226
|
+
consola.success(`Project "${data.name}" created successfully.`);
|
|
1227
|
+
return { uuid: data.uuid, name: data.name, deployment_hostnames: [] };
|
|
1228
|
+
}
|
|
1229
|
+
async function selectOrCreateInfrastructureProject(api, linkedProjectUuid) {
|
|
1230
|
+
consola.start("Fetching projects...");
|
|
1231
|
+
const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
|
|
1232
|
+
consola.success("Projects fetched.");
|
|
1233
|
+
if (existingProjects.length === 0) {
|
|
1234
|
+
const shouldCreate = await consola.prompt(
|
|
1235
|
+
"There are not any hosting projects that you can link to yet. Would you like to create one?",
|
|
1236
|
+
{ type: "confirm", initial: true }
|
|
1237
|
+
);
|
|
1238
|
+
if (!shouldCreate) {
|
|
1239
|
+
throw new NoLinkedProjectError();
|
|
1240
|
+
}
|
|
1241
|
+
return promptForNewProjectName(api);
|
|
1242
|
+
}
|
|
1243
|
+
const promptOptions = [
|
|
1244
|
+
...existingProjects.map((p) => ({
|
|
1245
|
+
label: p.uuid === linkedProjectUuid ? `${p.name} ${colorize2("green", "[linked]")}` : p.name,
|
|
1246
|
+
value: p.uuid,
|
|
1247
|
+
hint: p.uuid
|
|
1248
|
+
})),
|
|
1249
|
+
{
|
|
1250
|
+
label: "Create a new project",
|
|
1251
|
+
value: "create",
|
|
1252
|
+
hint: "Create a new hosting project for this Catalyst storefront."
|
|
1253
|
+
}
|
|
1254
|
+
];
|
|
1255
|
+
const selected = await consola.prompt(
|
|
1256
|
+
"Select a project or create a new project (Press <enter> to select).",
|
|
1257
|
+
{ type: "select", options: promptOptions, cancel: "reject" }
|
|
1258
|
+
);
|
|
1259
|
+
if (selected === "create") {
|
|
1260
|
+
return promptForNewProjectName(api);
|
|
1261
|
+
}
|
|
1262
|
+
const matched = existingProjects.find((p) => p.uuid === selected);
|
|
1263
|
+
if (!matched) {
|
|
1264
|
+
throw new Error(`Selected project ${String(selected)} not found in fetched list.`);
|
|
1265
|
+
}
|
|
1266
|
+
return matched;
|
|
1267
|
+
}
|
|
1268
|
+
async function promptForCommerceHostingProject(api, defaultName, autoUseDefaultName, useExistingOnCollision) {
|
|
1269
|
+
const existingProjects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
|
|
1270
|
+
const takenNames = existingProjects.map((project2) => project2.name);
|
|
1271
|
+
if (autoUseDefaultName) {
|
|
1272
|
+
return autoCreateCommerceHostingProject(
|
|
1273
|
+
api,
|
|
1274
|
+
defaultName,
|
|
1275
|
+
existingProjects,
|
|
1276
|
+
useExistingOnCollision
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
const conflict = existingProjects.find(
|
|
1280
|
+
(project2) => project2.name.toLowerCase() === defaultName.toLowerCase()
|
|
1281
|
+
);
|
|
1282
|
+
if (!conflict) {
|
|
1283
|
+
return autoCreateCommerceHostingProject(
|
|
1284
|
+
api,
|
|
1285
|
+
defaultName,
|
|
1286
|
+
existingProjects,
|
|
1287
|
+
useExistingOnCollision
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
const hasOtherProjects = existingProjects.length > 1;
|
|
1291
|
+
const choices = [
|
|
1292
|
+
{ name: `Use "${conflict.name}"`, value: "use-named" }
|
|
1293
|
+
];
|
|
1294
|
+
if (hasOtherProjects) {
|
|
1295
|
+
choices.push({ name: "Select from my projects", value: "select-from-list" });
|
|
1296
|
+
}
|
|
1297
|
+
choices.push({ name: "Create a new project", value: "create" });
|
|
1298
|
+
const action = await select({
|
|
1299
|
+
message: hasOtherProjects ? `It looks like you already have an existing Commerce Hosting project named "${conflict.name}". Would you like to use it, select from your projects, or create a new one?` : `It looks like you already have an existing Commerce Hosting project named "${conflict.name}". Would you like to use it, or create a new one?`,
|
|
1300
|
+
choices
|
|
1301
|
+
});
|
|
1302
|
+
if (action === "use-named") {
|
|
1303
|
+
consola.success(`Using existing Commerce Hosting project "${conflict.name}"`);
|
|
1304
|
+
return conflict;
|
|
1305
|
+
}
|
|
1306
|
+
if (action === "create") {
|
|
1307
|
+
return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
|
|
1308
|
+
}
|
|
1309
|
+
const selected = await select({
|
|
1310
|
+
message: "Which Commerce Hosting project would you like to use?",
|
|
1311
|
+
choices: [
|
|
1312
|
+
...existingProjects.map((project2) => ({
|
|
1313
|
+
name: project2.name,
|
|
1314
|
+
value: project2,
|
|
1315
|
+
description: project2.uuid
|
|
1316
|
+
})),
|
|
1317
|
+
new Separator(),
|
|
1318
|
+
{ name: "Create a new project", value: "create-new" }
|
|
1319
|
+
]
|
|
1320
|
+
});
|
|
1321
|
+
if (selected === "create-new") {
|
|
1322
|
+
return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
|
|
1323
|
+
}
|
|
1324
|
+
consola.success(`Using existing Commerce Hosting project "${selected.name}"`);
|
|
1325
|
+
return selected;
|
|
1326
|
+
}
|
|
1327
|
+
async function promptAndCreateCommerceHostingProject(api, takenNames, defaultName) {
|
|
1328
|
+
const projectName = await input({
|
|
1329
|
+
message: "What would you like to name your Commerce Hosting project?",
|
|
1330
|
+
default: defaultName,
|
|
1331
|
+
validate: (value) => {
|
|
1332
|
+
const trimmed = value.trim();
|
|
1333
|
+
if (!trimmed) return "Project name is required";
|
|
1334
|
+
const conflict = takenNames.find((taken) => taken.toLowerCase() === trimmed.toLowerCase());
|
|
1335
|
+
if (conflict) {
|
|
1336
|
+
return `A Commerce Hosting project named "${conflict}" already exists`;
|
|
1337
|
+
}
|
|
1338
|
+
return true;
|
|
1339
|
+
},
|
|
1340
|
+
theme: {
|
|
1341
|
+
style: {
|
|
1342
|
+
help: () => colorize2(
|
|
1343
|
+
"dim",
|
|
1344
|
+
"(The project that hosts your storefront on Commerce \u2014 often matches your folder name.)"
|
|
1345
|
+
)
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
try {
|
|
1350
|
+
const created = await createProject(
|
|
1351
|
+
projectName.trim(),
|
|
1352
|
+
api.storeHash,
|
|
1353
|
+
api.accessToken,
|
|
1354
|
+
api.apiHost
|
|
1355
|
+
);
|
|
1356
|
+
consola.success(`Commerce Hosting project "${created.name}" created successfully`);
|
|
1357
|
+
return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (error instanceof InfrastructureProjectValidationError) {
|
|
1360
|
+
consola.error(error.message);
|
|
1361
|
+
return promptAndCreateCommerceHostingProject(api, takenNames, defaultName);
|
|
1362
|
+
}
|
|
1363
|
+
throw error;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async function resolveCollisionChoice(existingName, useExistingOnCollision) {
|
|
1367
|
+
if (useExistingOnCollision === true) return true;
|
|
1368
|
+
if (!process.stdin.isTTY) return false;
|
|
1369
|
+
return select({
|
|
1370
|
+
message: `A Commerce Hosting project named "${existingName}" already exists. Use the existing project?`,
|
|
1371
|
+
choices: [
|
|
1372
|
+
{ name: "Yes", value: true },
|
|
1373
|
+
{ name: "No", value: false }
|
|
1374
|
+
]
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
async function autoCreateCommerceHostingProject(api, name, existingProjects, useExistingOnCollision) {
|
|
1378
|
+
const existing = existingProjects.find(
|
|
1379
|
+
(project2) => project2.name.toLowerCase() === name.toLowerCase()
|
|
1380
|
+
);
|
|
1381
|
+
if (existing) {
|
|
1382
|
+
const shouldUseExisting = await resolveCollisionChoice(existing.name, useExistingOnCollision);
|
|
1383
|
+
if (shouldUseExisting) {
|
|
1384
|
+
consola.success(`Using existing Commerce Hosting project "${existing.name}"`);
|
|
1385
|
+
return existing;
|
|
1386
|
+
}
|
|
1387
|
+
consola.error(
|
|
1388
|
+
"Not reusing the existing project. Re-run with a different --project-name, or pass --use-existing to reuse it."
|
|
1389
|
+
);
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
const created = await createProject(name, api.storeHash, api.accessToken, api.apiHost);
|
|
1394
|
+
consola.success(`Commerce Hosting project "${created.name}" created successfully`);
|
|
1395
|
+
return { uuid: created.uuid, name: created.name, deployment_hostnames: [] };
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (error instanceof InfrastructureProjectValidationError) {
|
|
1398
|
+
consola.error(
|
|
1399
|
+
`Failed to create Commerce Hosting project "${name}": ${error.message}
|
|
1400
|
+
Re-run with a different --project-name.`
|
|
1401
|
+
);
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/cli/lib/channel-site-flow.ts
|
|
1409
|
+
async function resolveProject(options) {
|
|
1410
|
+
const api = {
|
|
1411
|
+
storeHash: options.storeHash,
|
|
1412
|
+
accessToken: options.accessToken,
|
|
1413
|
+
apiHost: options.apiHost
|
|
1414
|
+
};
|
|
1415
|
+
if (options.projectUuid) {
|
|
1416
|
+
const projects = await fetchProjects(api.storeHash, api.accessToken, api.apiHost);
|
|
1417
|
+
const matched = projects.find((p) => p.uuid === options.projectUuid);
|
|
1418
|
+
if (matched) return matched;
|
|
1419
|
+
consola.warn(
|
|
1420
|
+
`Project ${options.projectUuid} not found on this store. Pick another to continue.`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
return selectOrCreateInfrastructureProject(api, options.projectUuid);
|
|
1424
|
+
}
|
|
1425
|
+
async function resolveChannel(options) {
|
|
1426
|
+
if (options.channelId !== void 0) {
|
|
1427
|
+
return { id: options.channelId };
|
|
1428
|
+
}
|
|
1429
|
+
consola.start("Fetching channels...");
|
|
1430
|
+
const channels = await fetchAvailableChannels(
|
|
1431
|
+
options.storeHash,
|
|
1432
|
+
options.accessToken,
|
|
1433
|
+
options.apiHost
|
|
1434
|
+
);
|
|
1435
|
+
consola.success("Channels fetched.");
|
|
1436
|
+
const catalystChannels = channels.filter((c) => c.platform === "catalyst");
|
|
1437
|
+
if (catalystChannels.length === 0) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
"No Catalyst channels found on this store. Create one with `catalyst create` and try again."
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
const selectedId = await consola.prompt("Select the channel to update.", {
|
|
1443
|
+
type: "select",
|
|
1444
|
+
options: catalystChannels.map((c) => ({
|
|
1445
|
+
label: c.name,
|
|
1446
|
+
value: String(c.id),
|
|
1447
|
+
hint: `id: ${c.id}`
|
|
1448
|
+
})),
|
|
1449
|
+
cancel: "reject"
|
|
1450
|
+
});
|
|
1451
|
+
const id = Number(selectedId);
|
|
1452
|
+
const matched = catalystChannels.find((c) => c.id === id);
|
|
1453
|
+
return { id, name: matched?.name };
|
|
1454
|
+
}
|
|
1455
|
+
async function resolveHostname(project2, options) {
|
|
1456
|
+
if (options.hostname) {
|
|
1457
|
+
return options.hostname;
|
|
1458
|
+
}
|
|
1459
|
+
if (project2.deployment_hostnames.length === 0) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
`Project "${project2.name}" has no deployment hostnames yet. Run \`catalyst deploy\` first to create one.`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
const ordered = options.preferHostname ? [
|
|
1465
|
+
...project2.deployment_hostnames.filter((h) => h === options.preferHostname),
|
|
1466
|
+
...project2.deployment_hostnames.filter((h) => h !== options.preferHostname)
|
|
1467
|
+
] : project2.deployment_hostnames;
|
|
1468
|
+
const selected = await consola.prompt("Select the hostname to point the channel at.", {
|
|
1469
|
+
type: "select",
|
|
1470
|
+
options: ordered.map((h) => ({ label: h, value: h })),
|
|
1471
|
+
cancel: "reject"
|
|
1472
|
+
});
|
|
1473
|
+
return String(selected);
|
|
1474
|
+
}
|
|
1475
|
+
async function runChannelSiteUrlFlow(options) {
|
|
1476
|
+
const project2 = await resolveProject(options);
|
|
1477
|
+
const channel2 = await resolveChannel(options);
|
|
1478
|
+
const hostname = await resolveHostname(project2, options);
|
|
1479
|
+
const siteUrl = hostname.startsWith("https://") ? hostname : `https://${hostname}`;
|
|
1480
|
+
await updateChannelSiteUrl(
|
|
1481
|
+
channel2.id,
|
|
1482
|
+
siteUrl,
|
|
1483
|
+
options.storeHash,
|
|
1484
|
+
options.accessToken,
|
|
1485
|
+
options.apiHost
|
|
1486
|
+
);
|
|
1487
|
+
const channelLabel = channel2.name ? `"${channel2.name}" (${channel2.id})` : String(channel2.id);
|
|
1488
|
+
consola.success(`Updated channel ${channelLabel} site URL to ${siteUrl}.`);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/cli/lib/resolve-credentials.ts
|
|
1492
|
+
function resolveCredentials(options, config2) {
|
|
1493
|
+
const storeHash = options.storeHash ?? config2.get("storeHash");
|
|
659
1494
|
const accessToken = options.accessToken ?? config2.get("accessToken");
|
|
660
1495
|
if (!storeHash || !accessToken) {
|
|
661
1496
|
consola.error("Missing credentials.");
|
|
@@ -668,8 +1503,690 @@ function resolveCredentials(options, config2) {
|
|
|
668
1503
|
return { storeHash, accessToken };
|
|
669
1504
|
}
|
|
670
1505
|
|
|
1506
|
+
// src/cli/commands/channel.ts
|
|
1507
|
+
var update = new Command3("update").configureHelp({ showGlobalOptions: true }).description(
|
|
1508
|
+
"Update a BigCommerce channel's site URL to point at one of your project's deployment hostnames."
|
|
1509
|
+
).addHelpText(
|
|
1510
|
+
"after",
|
|
1511
|
+
`
|
|
1512
|
+
Examples:
|
|
1513
|
+
# Pick a channel and hostname interactively
|
|
1514
|
+
$ catalyst channel update
|
|
1515
|
+
|
|
1516
|
+
# Skip both prompts
|
|
1517
|
+
$ catalyst channel update --channel-id 123 --hostname my-storefront.example.com`
|
|
1518
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
|
|
1519
|
+
new Option4(
|
|
1520
|
+
"--channel-id <id>",
|
|
1521
|
+
"Skip the channel prompt and target this channel directly."
|
|
1522
|
+
).argParser((value) => Number(value))
|
|
1523
|
+
).addOption(
|
|
1524
|
+
new Option4(
|
|
1525
|
+
"--hostname <hostname>",
|
|
1526
|
+
"Skip the hostname prompt and use this hostname directly. Must be one of the project's deployment_hostnames."
|
|
1527
|
+
)
|
|
1528
|
+
).action(async (options) => {
|
|
1529
|
+
const config2 = getProjectConfig();
|
|
1530
|
+
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
1531
|
+
await getTelemetry().identify(storeHash);
|
|
1532
|
+
try {
|
|
1533
|
+
await runChannelSiteUrlFlow({
|
|
1534
|
+
storeHash,
|
|
1535
|
+
accessToken,
|
|
1536
|
+
apiHost: options.apiHost,
|
|
1537
|
+
projectUuid: options.projectUuid ?? config2.get("projectUuid"),
|
|
1538
|
+
channelId: options.channelId,
|
|
1539
|
+
hostname: options.hostname
|
|
1540
|
+
});
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (error instanceof NoLinkedProjectError) {
|
|
1543
|
+
consola.info(
|
|
1544
|
+
"When you're ready to create a project, run `catalyst project create` or re-run `catalyst channel update`."
|
|
1545
|
+
);
|
|
1546
|
+
process.exit(0);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
throw error;
|
|
1550
|
+
}
|
|
1551
|
+
process.exit(0);
|
|
1552
|
+
});
|
|
1553
|
+
var channel = new Command3("channel").configureHelp({ showGlobalOptions: true }).description("Manage BigCommerce channels.").addCommand(update);
|
|
1554
|
+
|
|
1555
|
+
// src/cli/commands/create.ts
|
|
1556
|
+
import { Command as Command4, InvalidArgumentError, Option as Option5 } from "@commander-js/extra-typings";
|
|
1557
|
+
import { input as input2, select as select2 } from "@inquirer/prompts";
|
|
1558
|
+
import { execSync as execSync4 } from "child_process";
|
|
1559
|
+
import { colorize as colorize3 } from "consola/utils";
|
|
1560
|
+
import { pathExistsSync } from "fs-extra/esm";
|
|
1561
|
+
import kebabCase from "lodash.kebabcase";
|
|
1562
|
+
import { join as join9 } from "path";
|
|
1563
|
+
|
|
1564
|
+
// src/cli/lib/build-workspace-packages.ts
|
|
1565
|
+
import { execSync } from "child_process";
|
|
1566
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1567
|
+
import { join as join6 } from "path";
|
|
1568
|
+
import yoctoSpinner2 from "yocto-spinner";
|
|
1569
|
+
var WORKSPACE_PACKAGES = ["packages/catalyst", "packages/client"];
|
|
1570
|
+
var buildWorkspacePackages = (projectDir) => {
|
|
1571
|
+
const hasCore = existsSync3(join6(projectDir, "core"));
|
|
1572
|
+
const hasAllWorkspacePackages = WORKSPACE_PACKAGES.every(
|
|
1573
|
+
(pkg) => existsSync3(join6(projectDir, pkg))
|
|
1574
|
+
);
|
|
1575
|
+
if (!hasCore || !hasAllWorkspacePackages) return;
|
|
1576
|
+
WORKSPACE_PACKAGES.forEach((pkg) => {
|
|
1577
|
+
const spinner = yoctoSpinner2().start(`Building ${pkg}...`);
|
|
1578
|
+
try {
|
|
1579
|
+
execSync("pnpm build", { cwd: join6(projectDir, pkg), stdio: "ignore" });
|
|
1580
|
+
spinner.success(`Built ${pkg}.`);
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1583
|
+
spinner.error(`Failed to build ${pkg}: ${message}`);
|
|
1584
|
+
throw error;
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/cli/lib/clone-catalyst.ts
|
|
1590
|
+
import { execSync as execSync3 } from "child_process";
|
|
1591
|
+
|
|
1592
|
+
// src/cli/lib/checkout-ref.ts
|
|
1593
|
+
import { sync as spawnSync } from "cross-spawn";
|
|
1594
|
+
|
|
1595
|
+
// src/cli/lib/is-exec-exception.ts
|
|
1596
|
+
function isExecException(error) {
|
|
1597
|
+
return typeof error === "object" && error !== null && "stdout" in error && "stderr" in error;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/cli/lib/checkout-ref.ts
|
|
1601
|
+
function checkoutRef(repoDir, ref) {
|
|
1602
|
+
try {
|
|
1603
|
+
const spawn = spawnSync("git", ["checkout", ref, "--"], {
|
|
1604
|
+
cwd: repoDir,
|
|
1605
|
+
encoding: "utf8",
|
|
1606
|
+
shell: false
|
|
1607
|
+
});
|
|
1608
|
+
const stderr = spawn.stderr.trim();
|
|
1609
|
+
if (spawn.status !== 0 && stderr) {
|
|
1610
|
+
throw new Error(stderr);
|
|
1611
|
+
}
|
|
1612
|
+
consola.success(`Checked out ref ${ref} successfully.`);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
if (isExecException(error)) {
|
|
1615
|
+
const stderr = error.stderr ? error.stderr.toString() : "";
|
|
1616
|
+
if (stderr.includes(`fatal: reference is not a tree: ${ref}`) || stderr.includes(`fatal: ambiguous argument '${ref}'`) || stderr.includes(`unknown revision or path not in the working tree`)) {
|
|
1617
|
+
consola.error(`Ref '${ref}' not found in the repository.`);
|
|
1618
|
+
} else {
|
|
1619
|
+
consola.error(`Error checking out ref '${ref}':`, stderr.trim());
|
|
1620
|
+
}
|
|
1621
|
+
} else if (error instanceof Error) {
|
|
1622
|
+
consola.error(`Error checking out ref '${ref}':`, error.message);
|
|
1623
|
+
} else {
|
|
1624
|
+
consola.error(`Unknown error occurred while checking out ref '${ref}'.`);
|
|
1625
|
+
}
|
|
1626
|
+
consola.warn(`Falling back to the default branch.`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/cli/lib/has-github-ssh.ts
|
|
1631
|
+
import { execSync as execSync2 } from "child_process";
|
|
1632
|
+
function hasGitHubSSH() {
|
|
1633
|
+
try {
|
|
1634
|
+
const output = execSync2("ssh -T git@github.com", {
|
|
1635
|
+
encoding: "utf8",
|
|
1636
|
+
stdio: "pipe"
|
|
1637
|
+
}).toString();
|
|
1638
|
+
return output.includes("successfully authenticated");
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
if (isExecException(error)) {
|
|
1641
|
+
const stdout = error.stdout ? error.stdout.toString() : "";
|
|
1642
|
+
const stderr = error.stderr ? error.stderr.toString() : "";
|
|
1643
|
+
const combinedOutput = stdout + stderr;
|
|
1644
|
+
if (combinedOutput.includes("successfully authenticated")) {
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// src/cli/lib/reset-branch-to-ref.ts
|
|
1653
|
+
import { sync as spawnSync2 } from "cross-spawn";
|
|
1654
|
+
function resetBranchToRef(projectDir, ghRef) {
|
|
1655
|
+
const spawn = spawnSync2("git", ["reset", "--hard", ghRef, "--"], {
|
|
1656
|
+
cwd: projectDir,
|
|
1657
|
+
encoding: "utf8",
|
|
1658
|
+
shell: false
|
|
1659
|
+
});
|
|
1660
|
+
const stderr = spawn.stderr.trim();
|
|
1661
|
+
if (spawn.status !== 0 && stderr) {
|
|
1662
|
+
throw new Error(stderr);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/cli/lib/clone-catalyst.ts
|
|
1667
|
+
var cloneCatalyst = ({
|
|
1668
|
+
repository,
|
|
1669
|
+
projectName,
|
|
1670
|
+
projectDir,
|
|
1671
|
+
ghRef,
|
|
1672
|
+
resetMain = false
|
|
1673
|
+
}) => {
|
|
1674
|
+
const useSSH = hasGitHubSSH();
|
|
1675
|
+
consola.info(`Cloning ${repository} using ${useSSH ? "SSH" : "HTTPS"}...`);
|
|
1676
|
+
const cloneCommand = `git clone ${useSSH ? `git@github.com:${repository}` : `https://github.com/${repository}`}.git${projectName ? ` ${projectName}` : ""}`;
|
|
1677
|
+
execSync3(cloneCommand, { stdio: "inherit" });
|
|
1678
|
+
execSync3("git remote rename origin upstream", { cwd: projectDir, stdio: "inherit" });
|
|
1679
|
+
if (ghRef) {
|
|
1680
|
+
if (resetMain) {
|
|
1681
|
+
execSync3("git checkout -b main", { cwd: projectDir, stdio: "inherit" });
|
|
1682
|
+
resetBranchToRef(projectDir, ghRef);
|
|
1683
|
+
consola.success(`Reset main to ${ghRef} successfully.`);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
checkoutRef(projectDir, ghRef);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// src/cli/lib/install-dependencies.ts
|
|
1691
|
+
import { installDependencies as installDeps } from "nypm";
|
|
1692
|
+
import yoctoSpinner3 from "yocto-spinner";
|
|
1693
|
+
var installDependencies = async (projectDir) => {
|
|
1694
|
+
const spinner = yoctoSpinner3().start("Installing dependencies. This could take a minute...");
|
|
1695
|
+
try {
|
|
1696
|
+
await installDeps({ cwd: projectDir, silent: true, packageManager: "pnpm" });
|
|
1697
|
+
spinner.success("Dependencies installed successfully.");
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1700
|
+
spinner.error(`Failed to install dependencies: ${message}`);
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// src/cli/lib/localization.ts
|
|
1706
|
+
import { z as z8 } from "zod";
|
|
1707
|
+
var allowedLocales = [
|
|
1708
|
+
"en",
|
|
1709
|
+
"da",
|
|
1710
|
+
"es-AR",
|
|
1711
|
+
"es-CL",
|
|
1712
|
+
"es-CO",
|
|
1713
|
+
"es-MX",
|
|
1714
|
+
"es-PE",
|
|
1715
|
+
"es-419",
|
|
1716
|
+
"es",
|
|
1717
|
+
"it",
|
|
1718
|
+
"nl",
|
|
1719
|
+
"pl",
|
|
1720
|
+
"pt",
|
|
1721
|
+
"de",
|
|
1722
|
+
"fr",
|
|
1723
|
+
"ja",
|
|
1724
|
+
"no",
|
|
1725
|
+
"pt-BR",
|
|
1726
|
+
"sv"
|
|
1727
|
+
];
|
|
1728
|
+
var AvailableLocalesSuccessSchema = z8.object({
|
|
1729
|
+
data: z8.array(
|
|
1730
|
+
z8.object({
|
|
1731
|
+
id: z8.string(),
|
|
1732
|
+
name: z8.string(),
|
|
1733
|
+
fallback: z8.string().nullable(),
|
|
1734
|
+
is_supported: z8.boolean()
|
|
1735
|
+
})
|
|
1736
|
+
)
|
|
1737
|
+
});
|
|
1738
|
+
var getAvailableLocales = async (storeHash, accessToken, apiHost) => {
|
|
1739
|
+
const response = await fetch(
|
|
1740
|
+
`https://${apiHost}/stores/${storeHash}/v3/settings/store/available-locales`,
|
|
1741
|
+
{
|
|
1742
|
+
method: "GET",
|
|
1743
|
+
headers: {
|
|
1744
|
+
"X-Auth-Token": accessToken,
|
|
1745
|
+
"X-Correlation-Id": getTelemetry().correlationId,
|
|
1746
|
+
Accept: "application/json"
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1750
|
+
assertAuthorized(response);
|
|
1751
|
+
if (!response.ok) {
|
|
1752
|
+
throw new Error(
|
|
1753
|
+
`GET /v3/settings/store/available-locales failed: ${response.status} ${response.statusText}`
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
return AvailableLocalesSuccessSchema.parse(await response.json()).data.filter(({ id }) => allowedLocales.includes(id)).map(({ name, id }) => ({ name: `${name} (${id})`, value: id }));
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/cli/lib/setup-core-project.ts
|
|
1760
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1761
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
1762
|
+
import { z as z9 } from "zod";
|
|
1763
|
+
var corePackageJsonSchema2 = z9.looseObject({
|
|
1764
|
+
scripts: z9.record(z9.string(), z9.string()).optional(),
|
|
1765
|
+
dependencies: z9.record(z9.string(), z9.string()).optional()
|
|
1766
|
+
});
|
|
1767
|
+
var writeJson2 = (path, value) => {
|
|
1768
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1769
|
+
writeFileSync2(path, `${JSON.stringify(value, null, 2)}
|
|
1770
|
+
`);
|
|
1771
|
+
};
|
|
1772
|
+
var setupCoreProject = (projectDir) => {
|
|
1773
|
+
const corePackageJsonPath = join7(projectDir, "core", "package.json");
|
|
1774
|
+
const pkg = corePackageJsonSchema2.parse(JSON.parse(readFileSync3(corePackageJsonPath, "utf-8")));
|
|
1775
|
+
pkg.scripts = {
|
|
1776
|
+
...pkg.scripts,
|
|
1777
|
+
build: "npm run generate && catalyst build",
|
|
1778
|
+
start: "catalyst start",
|
|
1779
|
+
deploy: "npm run generate && catalyst deploy"
|
|
1780
|
+
};
|
|
1781
|
+
pkg.dependencies = {
|
|
1782
|
+
...pkg.dependencies,
|
|
1783
|
+
"@bigcommerce/catalyst": package_default.version
|
|
1784
|
+
};
|
|
1785
|
+
writeJson2(corePackageJsonPath, sortPackageJsonFields(pkg));
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
// src/cli/lib/write-env.ts
|
|
1789
|
+
import { outputFileSync } from "fs-extra/esm";
|
|
1790
|
+
import { join as join8 } from "path";
|
|
1791
|
+
var writeEnv = (projectDir, envVars) => {
|
|
1792
|
+
outputFileSync(
|
|
1793
|
+
join8(projectDir, "core", ".env.local"),
|
|
1794
|
+
`${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
1795
|
+
`
|
|
1796
|
+
);
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
// src/cli/commands/create.ts
|
|
1800
|
+
function getPlatformCheckCommand(command) {
|
|
1801
|
+
const isWindows = process.platform === "win32";
|
|
1802
|
+
return isWindows ? `where.exe ${command}` : `which ${command}`;
|
|
1803
|
+
}
|
|
1804
|
+
function parseChannelId(value) {
|
|
1805
|
+
const parsed = parseInt(value, 10);
|
|
1806
|
+
if (Number.isNaN(parsed)) {
|
|
1807
|
+
throw new InvalidArgumentError(`"${value}" is not a valid channel ID (expected a number).`);
|
|
1808
|
+
}
|
|
1809
|
+
return parsed;
|
|
1810
|
+
}
|
|
1811
|
+
function parseEnvFlag(value, previous = {}) {
|
|
1812
|
+
const eqIdx = value.indexOf("=");
|
|
1813
|
+
if (eqIdx <= 0) {
|
|
1814
|
+
throw new InvalidArgumentError(`Expected KEY=VALUE, got "${value}".`);
|
|
1815
|
+
}
|
|
1816
|
+
const key = value.substring(0, eqIdx);
|
|
1817
|
+
const val = value.substring(eqIdx + 1);
|
|
1818
|
+
if (!val) {
|
|
1819
|
+
throw new InvalidArgumentError(`Expected KEY=VALUE with non-empty value, got "${value}".`);
|
|
1820
|
+
}
|
|
1821
|
+
return { ...previous, [key]: val };
|
|
1822
|
+
}
|
|
1823
|
+
async function handleChannelCreation(storeHash, accessToken, apiHost, cliApiOrigin) {
|
|
1824
|
+
const newChannelName = await input2({
|
|
1825
|
+
message: "What would you like to name your new channel?"
|
|
1826
|
+
});
|
|
1827
|
+
const availableLocales = await getAvailableLocales(storeHash, accessToken, apiHost);
|
|
1828
|
+
const storefrontLocale = await select2({
|
|
1829
|
+
message: "Which default language would you like to set for your channel?",
|
|
1830
|
+
default: "en",
|
|
1831
|
+
choices: availableLocales,
|
|
1832
|
+
theme: {
|
|
1833
|
+
style: {
|
|
1834
|
+
help: () => colorize3("dim", "(Select locale from the list or start typing the name)")
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
const shouldAddAdditionalLocales = await select2({
|
|
1839
|
+
message: "Would you like to add additional languages?",
|
|
1840
|
+
choices: [
|
|
1841
|
+
{ name: "Yes", value: true },
|
|
1842
|
+
{ name: "No", value: false }
|
|
1843
|
+
]
|
|
1844
|
+
});
|
|
1845
|
+
let additionalLocales = [];
|
|
1846
|
+
if (shouldAddAdditionalLocales) {
|
|
1847
|
+
const localeOptions = availableLocales.filter(({ value }) => value !== storefrontLocale).map(({ name, value }) => ({ label: name, value, hint: value }));
|
|
1848
|
+
const pickLocales = async () => {
|
|
1849
|
+
const selected = await consola.prompt(
|
|
1850
|
+
"Which additional languages would you like to add to your channel?",
|
|
1851
|
+
{ type: "multiselect", options: localeOptions, cancel: "reject" }
|
|
1852
|
+
);
|
|
1853
|
+
if (selected.length > 4) {
|
|
1854
|
+
consola.warn("You can only select up to 4 additional languages. Please try again.");
|
|
1855
|
+
return pickLocales();
|
|
1856
|
+
}
|
|
1857
|
+
return selected;
|
|
1858
|
+
};
|
|
1859
|
+
additionalLocales = await pickLocales();
|
|
1860
|
+
}
|
|
1861
|
+
const shouldInstallSampleData = await select2({
|
|
1862
|
+
message: "Would you like to install sample data?",
|
|
1863
|
+
choices: [
|
|
1864
|
+
{ name: "Yes", value: true },
|
|
1865
|
+
{ name: "No", value: false }
|
|
1866
|
+
]
|
|
1867
|
+
});
|
|
1868
|
+
return createChannel(
|
|
1869
|
+
newChannelName,
|
|
1870
|
+
storefrontLocale,
|
|
1871
|
+
additionalLocales,
|
|
1872
|
+
shouldInstallSampleData,
|
|
1873
|
+
storeHash,
|
|
1874
|
+
accessToken,
|
|
1875
|
+
cliApiOrigin
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
async function handleChannelSelection(storeHash, accessToken, apiHost) {
|
|
1879
|
+
const channelSortOrder = ["catalyst", "next", "bigcommerce"];
|
|
1880
|
+
const channels = await fetchAvailableChannels(storeHash, accessToken, apiHost);
|
|
1881
|
+
const existingChannel = await select2({
|
|
1882
|
+
message: "Which channel would you like to use?",
|
|
1883
|
+
choices: channels.sort((a, b) => {
|
|
1884
|
+
const aIndex = channelSortOrder.indexOf(a.platform);
|
|
1885
|
+
const bIndex = channelSortOrder.indexOf(b.platform);
|
|
1886
|
+
if (aIndex === -1 && bIndex === -1) {
|
|
1887
|
+
return 0;
|
|
1888
|
+
}
|
|
1889
|
+
if (aIndex === -1) return 1;
|
|
1890
|
+
if (bIndex === -1) return -1;
|
|
1891
|
+
return aIndex - bIndex;
|
|
1892
|
+
}).map((ch) => ({
|
|
1893
|
+
name: ch.name,
|
|
1894
|
+
value: ch,
|
|
1895
|
+
description: `Channel Platform: ${ch.platform === "bigcommerce" ? "Stencil" : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1)}`
|
|
1896
|
+
}))
|
|
1897
|
+
});
|
|
1898
|
+
return existingChannel.id;
|
|
1899
|
+
}
|
|
1900
|
+
async function setupProject(options) {
|
|
1901
|
+
let { projectName, projectDir } = options;
|
|
1902
|
+
if (!pathExistsSync(projectDir)) {
|
|
1903
|
+
consola.error(`--project-dir ${projectDir} is not a valid path`);
|
|
1904
|
+
process.exit(1);
|
|
1905
|
+
}
|
|
1906
|
+
if (projectName) {
|
|
1907
|
+
projectName = kebabCase(projectName);
|
|
1908
|
+
projectDir = join9(options.projectDir, projectName);
|
|
1909
|
+
if (pathExistsSync(projectDir)) {
|
|
1910
|
+
consola.error(`${projectDir} already exists`);
|
|
1911
|
+
process.exit(1);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (!projectName) {
|
|
1915
|
+
const validateProjectName = (i) => {
|
|
1916
|
+
const formatted = kebabCase(i);
|
|
1917
|
+
if (!formatted) return "Project name is required";
|
|
1918
|
+
const targetDir = join9(options.projectDir, formatted);
|
|
1919
|
+
if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`;
|
|
1920
|
+
projectName = formatted;
|
|
1921
|
+
projectDir = targetDir;
|
|
1922
|
+
return true;
|
|
1923
|
+
};
|
|
1924
|
+
await input2({
|
|
1925
|
+
message: "What do you want to name your project directory?",
|
|
1926
|
+
default: "my-catalyst-app",
|
|
1927
|
+
validate: validateProjectName
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
if (!projectName) throw new Error("Something went wrong, projectName is not defined");
|
|
1931
|
+
if (!projectDir) throw new Error("Something went wrong, projectDir is not defined");
|
|
1932
|
+
return { projectName, projectDir };
|
|
1933
|
+
}
|
|
1934
|
+
function checkRequiredTools() {
|
|
1935
|
+
try {
|
|
1936
|
+
execSync4(getPlatformCheckCommand("git"), { stdio: "ignore" });
|
|
1937
|
+
} catch {
|
|
1938
|
+
consola.error("git is required to create a Catalyst project");
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
try {
|
|
1942
|
+
execSync4(getPlatformCheckCommand("pnpm"), { stdio: "ignore" });
|
|
1943
|
+
} catch {
|
|
1944
|
+
consola.error(
|
|
1945
|
+
"pnpm is required to create a Catalyst project. Enable it by running `corepack enable pnpm`."
|
|
1946
|
+
);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
var create = new Command4("create").configureHelp({ showGlobalOptions: true }).description("Scaffold and connect a Catalyst storefront to your BigCommerce store.").addHelpText(
|
|
1951
|
+
"after",
|
|
1952
|
+
`
|
|
1953
|
+
Examples:
|
|
1954
|
+
# Interactive scaffold (default \u2014 self-hosted, no hosting prompt)
|
|
1955
|
+
$ catalyst create
|
|
1956
|
+
|
|
1957
|
+
# Non-interactive: skip the project-name prompt
|
|
1958
|
+
$ catalyst create --project-name my-store
|
|
1959
|
+
|
|
1960
|
+
# Eagerly set up Commerce Hosting at create time
|
|
1961
|
+
$ catalyst create --project-name my-store --hosting commerce`
|
|
1962
|
+
).option("--project-name <name>", "Name of your Catalyst project").option("--project-dir <dir>", "Directory in which to create your project", process.cwd()).addOption(storeHashOption()).addOption(accessTokenOption()).option("--channel-id <id>", "BigCommerce channel ID", parseChannelId).option("--storefront-token <token>", "BigCommerce storefront token").option(
|
|
1963
|
+
"--gh-ref <ref>",
|
|
1964
|
+
"Clone a specific ref from the source repository",
|
|
1965
|
+
"@bigcommerce/catalyst-core@latest"
|
|
1966
|
+
).option("--reset-main", "Reset the main branch to the gh-ref").option("--repository <repository>", "GitHub repository to clone from", "bigcommerce/catalyst").option(
|
|
1967
|
+
"--env <vars...>",
|
|
1968
|
+
"Arbitrary environment variables to set in .env.local. Format: KEY=VALUE (repeatable).",
|
|
1969
|
+
parseEnvFlag
|
|
1970
|
+
).addOption(
|
|
1971
|
+
new Option5(
|
|
1972
|
+
"--hosting <mode>",
|
|
1973
|
+
'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.'
|
|
1974
|
+
).choices(["self-hosted", "commerce"])
|
|
1975
|
+
).option(
|
|
1976
|
+
"--use-existing",
|
|
1977
|
+
"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."
|
|
1978
|
+
).addOption(
|
|
1979
|
+
new Option5("--bigcommerce-hostname <hostname>", "BigCommerce hostname").default("bigcommerce.com").hideHelp()
|
|
1980
|
+
).addOption(
|
|
1981
|
+
new Option5("--login-url <url>", "BigCommerce login URL.").env("BIGCOMMERCE_LOGIN_URL").default(DEFAULT_LOGIN_URL).hideHelp()
|
|
1982
|
+
).addOption(
|
|
1983
|
+
new Option5("--cli-api-origin <origin>", "Catalyst CLI API origin").default("https://cxm-prd.bigcommerceapp.com").hideHelp()
|
|
1984
|
+
).action(async (options) => {
|
|
1985
|
+
const { ghRef, repository } = options;
|
|
1986
|
+
if (options.useExisting && options.hosting !== "commerce") {
|
|
1987
|
+
consola.warn("--use-existing has no effect without --hosting commerce. Ignoring.");
|
|
1988
|
+
}
|
|
1989
|
+
checkRequiredTools();
|
|
1990
|
+
const { projectName, projectDir } = await setupProject({
|
|
1991
|
+
projectName: options.projectName,
|
|
1992
|
+
projectDir: options.projectDir
|
|
1993
|
+
});
|
|
1994
|
+
let storeHash = options.storeHash;
|
|
1995
|
+
let accessToken = options.accessToken;
|
|
1996
|
+
let channelId = options.channelId;
|
|
1997
|
+
let storefrontToken = options.storefrontToken;
|
|
1998
|
+
let envVars = {};
|
|
1999
|
+
if (!storeHash || !accessToken) {
|
|
2000
|
+
const apiHost = `api.${options.bigcommerceHostname}`;
|
|
2001
|
+
try {
|
|
2002
|
+
const credentials = await login(options.loginUrl, apiHost);
|
|
2003
|
+
storeHash = credentials.storeHash;
|
|
2004
|
+
accessToken = credentials.accessToken;
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
if (error instanceof LoginAbortedError) {
|
|
2007
|
+
consola.info(
|
|
2008
|
+
"Login aborted. Re-run `catalyst create` when you have your credentials ready."
|
|
2009
|
+
);
|
|
2010
|
+
process.exit(0);
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
const useCommerceHosting = options.hosting === "commerce";
|
|
2017
|
+
await getTelemetry().identify(storeHash);
|
|
2018
|
+
if (storeHash && channelId && storefrontToken) {
|
|
2019
|
+
envVars.BIGCOMMERCE_STORE_HASH = storeHash;
|
|
2020
|
+
envVars.BIGCOMMERCE_CHANNEL_ID = channelId.toString();
|
|
2021
|
+
envVars.BIGCOMMERCE_STOREFRONT_TOKEN = storefrontToken;
|
|
2022
|
+
}
|
|
2023
|
+
if (storeHash && accessToken && (!channelId || !storefrontToken)) {
|
|
2024
|
+
const apiHost = `api.${options.bigcommerceHostname}`;
|
|
2025
|
+
const cliApiOrigin = options.cliApiOrigin;
|
|
2026
|
+
if (channelId && !storefrontToken) {
|
|
2027
|
+
const initData = await getChannelInit(channelId, storeHash, accessToken, cliApiOrigin);
|
|
2028
|
+
envVars = { ...initData.envVars };
|
|
2029
|
+
storefrontToken = initData.storefrontToken;
|
|
2030
|
+
} else if (!channelId) {
|
|
2031
|
+
const eligibility = await checkChannelEligibility(storeHash, accessToken, cliApiOrigin);
|
|
2032
|
+
if (!eligibility.eligible) {
|
|
2033
|
+
consola.warn(eligibility.message);
|
|
2034
|
+
}
|
|
2035
|
+
let shouldCreateChannel;
|
|
2036
|
+
if (eligibility.eligible) {
|
|
2037
|
+
shouldCreateChannel = await select2({
|
|
2038
|
+
message: "Would you like to create a new channel?",
|
|
2039
|
+
choices: [
|
|
2040
|
+
{ name: "Yes", value: true },
|
|
2041
|
+
{ name: "No", value: false }
|
|
2042
|
+
]
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
if (shouldCreateChannel) {
|
|
2046
|
+
const channelData = await handleChannelCreation(
|
|
2047
|
+
storeHash,
|
|
2048
|
+
accessToken,
|
|
2049
|
+
apiHost,
|
|
2050
|
+
cliApiOrigin
|
|
2051
|
+
);
|
|
2052
|
+
channelId = channelData.channelId;
|
|
2053
|
+
storefrontToken = channelData.storefrontToken;
|
|
2054
|
+
envVars = { ...channelData.envVars };
|
|
2055
|
+
consola.success("Channel created successfully.");
|
|
2056
|
+
consola.warn(
|
|
2057
|
+
"A preview storefront has been deployed in your BigCommerce control panel. This preview may look different from your local environment as it may be running different code. Additionally, it may take a few minutes for the channel storefront to be accessible."
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
if (!shouldCreateChannel) {
|
|
2061
|
+
channelId = await handleChannelSelection(storeHash, accessToken, apiHost);
|
|
2062
|
+
const initData = await getChannelInit(channelId, storeHash, accessToken, cliApiOrigin);
|
|
2063
|
+
envVars = { ...initData.envVars };
|
|
2064
|
+
storefrontToken = initData.storefrontToken;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (options.env) {
|
|
2069
|
+
Object.assign(envVars, options.env);
|
|
2070
|
+
}
|
|
2071
|
+
if (options.storeHash) envVars.BIGCOMMERCE_STORE_HASH = options.storeHash;
|
|
2072
|
+
if (options.channelId) envVars.BIGCOMMERCE_CHANNEL_ID = options.channelId.toString();
|
|
2073
|
+
if (options.storefrontToken) envVars.BIGCOMMERCE_STOREFRONT_TOKEN = options.storefrontToken;
|
|
2074
|
+
if (useCommerceHosting && accessToken) {
|
|
2075
|
+
envVars.BIGCOMMERCE_ACCESS_TOKEN = accessToken;
|
|
2076
|
+
}
|
|
2077
|
+
if (accessToken) envVars.CATALYST_ACCESS_TOKEN = accessToken;
|
|
2078
|
+
let commerceHostingProject;
|
|
2079
|
+
if (useCommerceHosting && storeHash && accessToken) {
|
|
2080
|
+
const apiHost = `api.${options.bigcommerceHostname}`;
|
|
2081
|
+
const hasAccess = await hasProjectsAccess(storeHash, accessToken, apiHost);
|
|
2082
|
+
if (!hasAccess) {
|
|
2083
|
+
consola.error(
|
|
2084
|
+
"This store does not have access to the Infrastructure Projects API. Contact support@bigcommerce.com to enable it."
|
|
2085
|
+
);
|
|
2086
|
+
process.exit(1);
|
|
2087
|
+
}
|
|
2088
|
+
commerceHostingProject = await promptForCommerceHostingProject(
|
|
2089
|
+
{ storeHash, accessToken, apiHost },
|
|
2090
|
+
projectName,
|
|
2091
|
+
!!options.projectName,
|
|
2092
|
+
options.useExisting
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
consola.info(`Creating '${projectName}' at '${projectDir}'`);
|
|
2096
|
+
try {
|
|
2097
|
+
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
|
|
2098
|
+
setupCoreProject(projectDir);
|
|
2099
|
+
if (useCommerceHosting && commerceHostingProject && storeHash && accessToken) {
|
|
2100
|
+
await setupCommerceHosting({
|
|
2101
|
+
projectDir,
|
|
2102
|
+
projectUuid: commerceHostingProject.uuid,
|
|
2103
|
+
storeHash,
|
|
2104
|
+
accessToken
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
writeEnv(projectDir, envVars);
|
|
2108
|
+
await installDependencies(projectDir);
|
|
2109
|
+
buildWorkspacePackages(projectDir);
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
if (pathExistsSync(projectDir)) {
|
|
2112
|
+
consola.warn(
|
|
2113
|
+
`Setup failed before completion. '${projectDir}' may be in a partial state \u2014 review and delete it before re-running 'catalyst create'.`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
throw error;
|
|
2117
|
+
}
|
|
2118
|
+
consola.success(`Created '${projectName}' at '${projectDir}'`);
|
|
2119
|
+
consola.info("Next steps:");
|
|
2120
|
+
consola.info(colorize3("yellow", ` cd ${projectName}/core && pnpm run dev`));
|
|
2121
|
+
if (useCommerceHosting) {
|
|
2122
|
+
consola.info(
|
|
2123
|
+
colorize3(
|
|
2124
|
+
"yellow",
|
|
2125
|
+
` Run 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce Hosting.`
|
|
2126
|
+
)
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
// src/cli/commands/deploy.ts
|
|
2132
|
+
import AdmZip from "adm-zip";
|
|
2133
|
+
import { Command as Command5, Option as Option6 } from "commander";
|
|
2134
|
+
import { colorize as colorize4 } from "consola/utils";
|
|
2135
|
+
import { access, readdir, readFile } from "fs/promises";
|
|
2136
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
2137
|
+
import yoctoSpinner4 from "yocto-spinner";
|
|
2138
|
+
import { z as z10 } from "zod";
|
|
2139
|
+
|
|
2140
|
+
// src/cli/lib/deployment-errors.ts
|
|
2141
|
+
var ERROR_MESSAGES = {
|
|
2142
|
+
10: "Something went wrong on our end. Please try again. If the issue persists, contact support.",
|
|
2143
|
+
20: "We couldn't retrieve your bundle. This is usually a temporary issue \u2014 please try deploying again. If the problem continues, contact support.",
|
|
2144
|
+
30: "Your bundle could not be extracted. This may mean your build output is too large (max 64 MB compressed / 512 MB uncompressed) or the archive is corrupted. Try reducing your build size or rebuilding your project and deploying again.",
|
|
2145
|
+
40: `There's a problem with your build output. This could be caused by:
|
|
2146
|
+
- A worker.js file larger than 40 MB
|
|
2147
|
+
- An individual asset file larger than 25 MB
|
|
2148
|
+
- More than 1,000 total files in the bundle`,
|
|
2149
|
+
50: "Deployment failed. This is usually a temporary issue \u2014 please try again. If the problem persists, contact support.",
|
|
2150
|
+
60: "Your code was deployed, but we couldn't determine your deployment URL. Please try deploying again. If the issue persists, contact support."
|
|
2151
|
+
};
|
|
2152
|
+
function getDeploymentErrorMessage(code) {
|
|
2153
|
+
return ERROR_MESSAGES[code] ?? `Deployment failed with an unexpected error (code: ${code}). Please try again. If the issue persists, contact support.`;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// src/cli/lib/env-config.ts
|
|
2157
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2158
|
+
var parseEnvAssignment = (input3) => {
|
|
2159
|
+
const separatorIndex = input3.indexOf("=");
|
|
2160
|
+
if (separatorIndex === -1) {
|
|
2161
|
+
throw new Error(`Invalid env var format: ${input3}. Expected format: KEY=VALUE`);
|
|
2162
|
+
}
|
|
2163
|
+
const key = input3.slice(0, separatorIndex).trim();
|
|
2164
|
+
const value = input3.slice(separatorIndex + 1).trim();
|
|
2165
|
+
if (!key || !value) {
|
|
2166
|
+
throw new Error(`Invalid env var format: ${input3}. Expected format: KEY=VALUE`);
|
|
2167
|
+
}
|
|
2168
|
+
if (!ENV_KEY_PATTERN.test(key)) {
|
|
2169
|
+
throw new Error(
|
|
2170
|
+
`Invalid env var name: ${key}. Names must start with a letter or underscore and contain only letters, numbers, and underscores.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
return { key, value };
|
|
2174
|
+
};
|
|
2175
|
+
var getStoredEnv = (config2) => {
|
|
2176
|
+
return config2.get("env") ?? {};
|
|
2177
|
+
};
|
|
2178
|
+
var toDeploymentSecrets = (envVars) => {
|
|
2179
|
+
return Object.entries(envVars).map(([key, value]) => ({ type: "secret", key, value }));
|
|
2180
|
+
};
|
|
2181
|
+
var mergeDeploymentSecrets = (persisted, flagSecrets) => {
|
|
2182
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2183
|
+
persisted.forEach((secret) => byKey.set(secret.key, secret));
|
|
2184
|
+
flagSecrets.forEach((secret) => byKey.set(secret.key, secret));
|
|
2185
|
+
return Array.from(byKey.values());
|
|
2186
|
+
};
|
|
2187
|
+
|
|
671
2188
|
// src/cli/commands/deploy.ts
|
|
672
|
-
var stepsEnum =
|
|
2189
|
+
var stepsEnum = z10.enum([
|
|
673
2190
|
"initializing",
|
|
674
2191
|
"downloading",
|
|
675
2192
|
"unzipping",
|
|
@@ -687,41 +2204,41 @@ var STEPS = {
|
|
|
687
2204
|
finalizing: "Finalizing...",
|
|
688
2205
|
complete: "Complete"
|
|
689
2206
|
};
|
|
690
|
-
var UploadSignatureSchema =
|
|
691
|
-
data:
|
|
692
|
-
upload_url:
|
|
693
|
-
upload_uuid:
|
|
2207
|
+
var UploadSignatureSchema = z10.object({
|
|
2208
|
+
data: z10.object({
|
|
2209
|
+
upload_url: z10.url(),
|
|
2210
|
+
upload_uuid: z10.string()
|
|
694
2211
|
})
|
|
695
2212
|
});
|
|
696
|
-
var CreateDeploymentSchema =
|
|
697
|
-
data:
|
|
698
|
-
deployment_uuid:
|
|
2213
|
+
var CreateDeploymentSchema = z10.object({
|
|
2214
|
+
data: z10.object({
|
|
2215
|
+
deployment_uuid: z10.uuid()
|
|
699
2216
|
})
|
|
700
2217
|
});
|
|
701
|
-
var ProjectSchema =
|
|
702
|
-
data:
|
|
703
|
-
|
|
704
|
-
uuid:
|
|
705
|
-
name:
|
|
2218
|
+
var ProjectSchema = z10.object({
|
|
2219
|
+
data: z10.array(
|
|
2220
|
+
z10.object({
|
|
2221
|
+
uuid: z10.uuid(),
|
|
2222
|
+
name: z10.string()
|
|
706
2223
|
})
|
|
707
2224
|
)
|
|
708
2225
|
});
|
|
709
|
-
var DeploymentStatusSchema =
|
|
710
|
-
deployment_uuid:
|
|
711
|
-
deployment_status:
|
|
712
|
-
event:
|
|
2226
|
+
var DeploymentStatusSchema = z10.object({
|
|
2227
|
+
deployment_uuid: z10.uuid(),
|
|
2228
|
+
deployment_status: z10.enum(["queued", "in_progress", "failed", "completed"]),
|
|
2229
|
+
event: z10.object({
|
|
713
2230
|
step: stepsEnum,
|
|
714
|
-
progress:
|
|
2231
|
+
progress: z10.number()
|
|
715
2232
|
}).nullable(),
|
|
716
|
-
|
|
717
|
-
error:
|
|
718
|
-
code:
|
|
2233
|
+
deployment_hostnames: z10.array(z10.string()).optional(),
|
|
2234
|
+
error: z10.object({
|
|
2235
|
+
code: z10.number()
|
|
719
2236
|
}).optional()
|
|
720
2237
|
});
|
|
721
2238
|
var generateBundleZip = async () => {
|
|
722
2239
|
consola.info("Generating bundle...");
|
|
723
|
-
const bigcommerceDir =
|
|
724
|
-
const distDir =
|
|
2240
|
+
const bigcommerceDir = join10(process.cwd(), ".bigcommerce");
|
|
2241
|
+
const distDir = join10(process.cwd(), ".bigcommerce", "dist");
|
|
725
2242
|
try {
|
|
726
2243
|
await access(distDir);
|
|
727
2244
|
} catch {
|
|
@@ -731,7 +2248,7 @@ var generateBundleZip = async () => {
|
|
|
731
2248
|
if (buildDirContents.length === 0) {
|
|
732
2249
|
throw new Error(`Dist directory is empty: ${distDir}`);
|
|
733
2250
|
}
|
|
734
|
-
const outputZip =
|
|
2251
|
+
const outputZip = join10(bigcommerceDir, "bundle.zip");
|
|
735
2252
|
const zip = new AdmZip();
|
|
736
2253
|
zip.addLocalFolder(distDir, "output");
|
|
737
2254
|
zip.writeZip(outputZip);
|
|
@@ -752,6 +2269,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
|
|
|
752
2269
|
body: JSON.stringify({})
|
|
753
2270
|
}
|
|
754
2271
|
);
|
|
2272
|
+
assertAuthorized(response);
|
|
755
2273
|
if (!response.ok) {
|
|
756
2274
|
throw new Error(`Failed to fetch upload signature: ${response.status} ${response.statusText}`);
|
|
757
2275
|
}
|
|
@@ -762,7 +2280,7 @@ var generateUploadSignature = async (storeHash, accessToken, apiHost) => {
|
|
|
762
2280
|
};
|
|
763
2281
|
var uploadBundleZip = async (uploadUrl) => {
|
|
764
2282
|
consola.info("Uploading bundle...");
|
|
765
|
-
const zipPath =
|
|
2283
|
+
const zipPath = join10(process.cwd(), ".bigcommerce", "bundle.zip");
|
|
766
2284
|
const fileBuffer = await readFile(zipPath);
|
|
767
2285
|
const response = await fetch(uploadUrl, {
|
|
768
2286
|
method: "PUT",
|
|
@@ -779,41 +2297,14 @@ var uploadBundleZip = async (uploadUrl) => {
|
|
|
779
2297
|
};
|
|
780
2298
|
var parseEnvironmentVariables = (secretOption) => {
|
|
781
2299
|
return secretOption?.map((envVar) => {
|
|
782
|
-
const
|
|
783
|
-
if (!key || !value) {
|
|
784
|
-
throw new Error(`Invalid secret format: ${envVar}. Expected format: KEY=VALUE`);
|
|
785
|
-
}
|
|
2300
|
+
const { key, value } = parseEnvAssignment(envVar);
|
|
786
2301
|
return {
|
|
787
2302
|
type: "secret",
|
|
788
|
-
key
|
|
789
|
-
value
|
|
2303
|
+
key,
|
|
2304
|
+
value
|
|
790
2305
|
};
|
|
791
2306
|
});
|
|
792
2307
|
};
|
|
793
|
-
var AUTO_DETECT_SECRETS = [
|
|
794
|
-
{ key: "BIGCOMMERCE_STORE_HASH", warnIfMissing: true },
|
|
795
|
-
{ key: "BIGCOMMERCE_CHANNEL_ID", warnIfMissing: true },
|
|
796
|
-
{ key: "BIGCOMMERCE_STOREFRONT_TOKEN", warnIfMissing: true },
|
|
797
|
-
{ key: "BIGCOMMERCE_API_HOST", warnIfMissing: false },
|
|
798
|
-
{ key: "BIGCOMMERCE_GRAPHQL_API_DOMAIN", warnIfMissing: false },
|
|
799
|
-
{ key: "AUTH_SECRET", warnIfMissing: false }
|
|
800
|
-
];
|
|
801
|
-
var autoDetectSecrets = (environmentVariables) => {
|
|
802
|
-
const secrets = environmentVariables ?? [];
|
|
803
|
-
const existingKeys = new Set(secrets.map((s) => s.key));
|
|
804
|
-
AUTO_DETECT_SECRETS.forEach(({ key, warnIfMissing }) => {
|
|
805
|
-
if (existingKeys.has(key)) {
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
const value = process.env[key];
|
|
809
|
-
if (value) {
|
|
810
|
-
secrets.push({ type: "secret", key, value });
|
|
811
|
-
} else if (warnIfMissing) {
|
|
812
|
-
consola.warn(`${key} is not set in the environment and was not provided via --secret.`);
|
|
813
|
-
}
|
|
814
|
-
});
|
|
815
|
-
return secrets;
|
|
816
|
-
};
|
|
817
2308
|
var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, apiHost, environmentVariables) => {
|
|
818
2309
|
consola.info("Creating deployment...");
|
|
819
2310
|
const response = await fetch(
|
|
@@ -833,6 +2324,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
|
|
|
833
2324
|
})
|
|
834
2325
|
}
|
|
835
2326
|
);
|
|
2327
|
+
assertAuthorized(response);
|
|
836
2328
|
if (!response.ok) {
|
|
837
2329
|
throw new Error(`Failed to create deployment: ${response.status} ${response.statusText}`);
|
|
838
2330
|
}
|
|
@@ -843,7 +2335,7 @@ var createDeployment = async (projectUuid, uploadUuid, storeHash, accessToken, a
|
|
|
843
2335
|
};
|
|
844
2336
|
var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost) => {
|
|
845
2337
|
consola.info("Fetching deployment status...");
|
|
846
|
-
const spinner =
|
|
2338
|
+
const spinner = yoctoSpinner4().start("Fetching...");
|
|
847
2339
|
const response = await fetch(
|
|
848
2340
|
`https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments/${deploymentUuid}/events`,
|
|
849
2341
|
{
|
|
@@ -856,6 +2348,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
|
|
|
856
2348
|
}
|
|
857
2349
|
}
|
|
858
2350
|
);
|
|
2351
|
+
assertAuthorized(response);
|
|
859
2352
|
if (!response.ok) {
|
|
860
2353
|
throw new Error(`Failed to open event stream: ${response.status} ${response.statusText}`);
|
|
861
2354
|
}
|
|
@@ -865,7 +2358,7 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
|
|
|
865
2358
|
}
|
|
866
2359
|
const decoder = new TextDecoder();
|
|
867
2360
|
let done = false;
|
|
868
|
-
let
|
|
2361
|
+
let deploymentHostname;
|
|
869
2362
|
while (!done) {
|
|
870
2363
|
const { value, done: streamDone } = await reader.read();
|
|
871
2364
|
let json;
|
|
@@ -888,40 +2381,57 @@ var getDeploymentStatus = async (deploymentUuid, storeHash, accessToken, apiHost
|
|
|
888
2381
|
if (data.event && STEPS[data.event.step] !== spinner.text) {
|
|
889
2382
|
spinner.text = STEPS[data.event.step];
|
|
890
2383
|
}
|
|
891
|
-
if (data.
|
|
892
|
-
|
|
2384
|
+
if (data.deployment_hostnames && data.deployment_hostnames.length > 0) {
|
|
2385
|
+
deploymentHostname = data.deployment_hostnames[0];
|
|
893
2386
|
}
|
|
894
2387
|
});
|
|
895
2388
|
}
|
|
896
2389
|
done = streamDone;
|
|
897
2390
|
}
|
|
898
2391
|
spinner.success("Deployment completed successfully.");
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
|
|
2392
|
+
if (deploymentHostname) {
|
|
2393
|
+
consola.success(
|
|
2394
|
+
`View your deployment at: ${colorize4("blue", `https://${deploymentHostname}`)}`
|
|
2395
|
+
);
|
|
902
2396
|
}
|
|
2397
|
+
return deploymentHostname;
|
|
903
2398
|
};
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
2399
|
+
var fetchProject = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
2400
|
+
const response = await fetch(
|
|
2401
|
+
`https://${apiHost}/stores/${storeHash}/v3/infrastructure/projects`,
|
|
2402
|
+
{
|
|
2403
|
+
method: "GET",
|
|
2404
|
+
headers: {
|
|
2405
|
+
"X-Auth-Token": accessToken,
|
|
2406
|
+
"Content-Type": "application/json",
|
|
2407
|
+
Accept: "application/json",
|
|
2408
|
+
"X-Correlation-Id": getTelemetry().correlationId
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
);
|
|
2412
|
+
assertAuthorized(response);
|
|
2413
|
+
if (!response.ok) {
|
|
2414
|
+
throw new Error(`Failed to fetch projects: ${response.status} ${response.statusText}`);
|
|
2415
|
+
}
|
|
2416
|
+
const res = await response.json();
|
|
2417
|
+
const { data } = ProjectSchema.parse(res);
|
|
2418
|
+
return data.find((project2) => project2.uuid === projectUuid);
|
|
2419
|
+
};
|
|
2420
|
+
var deploy = new Command5("deploy").configureHelp({ showGlobalOptions: true }).description("Deploy your application to Cloudflare.").addHelpText(
|
|
2421
|
+
"after",
|
|
2422
|
+
`
|
|
2423
|
+
Environment variables saved with \`catalyst env add\` are sent automatically on every deploy.
|
|
2424
|
+
Use \`--secret\` to set or override a variable for a single run.
|
|
2425
|
+
|
|
2426
|
+
Example:
|
|
2427
|
+
$ catalyst deploy --secret BIGCOMMERCE_STORE_HASH=<YOUR_STORE_HASH> --secret BIGCOMMERCE_STOREFRONT_TOKEN=<YOUR_STOREFRONT_TOKEN>`
|
|
2428
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option(
|
|
2429
|
+
"--update-site-url",
|
|
2430
|
+
"After a successful deploy, prompt to update a channel's site URL to the new hostname."
|
|
921
2431
|
).addOption(
|
|
922
|
-
new
|
|
2432
|
+
new Option6(
|
|
923
2433
|
"--secret <value>",
|
|
924
|
-
"Secret to set for
|
|
2434
|
+
"Secret to set for this deployment (repeatable). Overrides a stored value for the same key. Format: --secret KEY=VALUE"
|
|
925
2435
|
).argParser((value, previous = []) => {
|
|
926
2436
|
return previous.concat([value]);
|
|
927
2437
|
})
|
|
@@ -932,15 +2442,69 @@ var deploy = new Command3("deploy").description("Deploy your application to Clou
|
|
|
932
2442
|
const config2 = getProjectConfig();
|
|
933
2443
|
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
934
2444
|
const telemetry3 = getTelemetry();
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
2445
|
+
await telemetry3.identify(storeHash);
|
|
2446
|
+
const linkedProjectUuid = options.projectUuid ?? config2.get("projectUuid");
|
|
2447
|
+
let projectUuid;
|
|
2448
|
+
const promptForProject = async () => {
|
|
2449
|
+
try {
|
|
2450
|
+
return await selectOrCreateInfrastructureProject({
|
|
2451
|
+
storeHash,
|
|
2452
|
+
accessToken,
|
|
2453
|
+
apiHost: options.apiHost
|
|
2454
|
+
});
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
if (error instanceof NoLinkedProjectError) {
|
|
2457
|
+
consola.info(
|
|
2458
|
+
"When you're ready to create a project, run `catalyst project create` or re-run `catalyst deploy`."
|
|
2459
|
+
);
|
|
2460
|
+
process.exit(0);
|
|
2461
|
+
}
|
|
2462
|
+
throw error;
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
if (linkedProjectUuid) {
|
|
2466
|
+
const existing = await fetchProject(
|
|
2467
|
+
linkedProjectUuid,
|
|
2468
|
+
storeHash,
|
|
2469
|
+
accessToken,
|
|
2470
|
+
options.apiHost
|
|
2471
|
+
);
|
|
2472
|
+
if (existing) {
|
|
2473
|
+
projectUuid = linkedProjectUuid;
|
|
2474
|
+
} else {
|
|
2475
|
+
consola.warn(
|
|
2476
|
+
`The linked project (${linkedProjectUuid}) no longer exists on this store. It may have been deleted.`
|
|
2477
|
+
);
|
|
2478
|
+
const selected = await promptForProject();
|
|
2479
|
+
projectUuid = selected.uuid;
|
|
2480
|
+
config2.set("projectUuid", projectUuid);
|
|
2481
|
+
consola.success(`Linked project "${selected.name}".`);
|
|
2482
|
+
}
|
|
2483
|
+
} else {
|
|
2484
|
+
consola.info("No project is currently linked.");
|
|
2485
|
+
const selected = await promptForProject();
|
|
2486
|
+
projectUuid = selected.uuid;
|
|
2487
|
+
config2.set("projectUuid", projectUuid);
|
|
2488
|
+
consola.success(`Linked project "${selected.name}".`);
|
|
2489
|
+
}
|
|
2490
|
+
if (!getProjectState().isTransformed) {
|
|
2491
|
+
const shouldSetup = await consola.prompt(
|
|
2492
|
+
"Your project is not yet set up for Commerce Hosting deployments. Would you like to run the Commerce Hosting setup now?",
|
|
2493
|
+
{ type: "confirm", initial: true }
|
|
939
2494
|
);
|
|
2495
|
+
if (!shouldSetup) {
|
|
2496
|
+
consola.info("When you're ready to deploy, re-run `catalyst deploy` to complete setup.");
|
|
2497
|
+
process.exit(0);
|
|
2498
|
+
}
|
|
2499
|
+
const projectDir = dirname4(process.cwd());
|
|
2500
|
+
await setupCommerceHosting({ projectDir, projectUuid, storeHash, accessToken });
|
|
2501
|
+
consola.success("Commerce Hosting setup complete.");
|
|
2502
|
+
await installDependencies(projectDir);
|
|
2503
|
+
} else {
|
|
2504
|
+
await cleanupCloudflareIncompatibilities(dirname4(process.cwd()));
|
|
940
2505
|
}
|
|
941
|
-
await telemetry3.identify(storeHash);
|
|
942
2506
|
if (options.prebuilt) {
|
|
943
|
-
const distDir =
|
|
2507
|
+
const distDir = join10(process.cwd(), ".bigcommerce", "dist");
|
|
944
2508
|
try {
|
|
945
2509
|
await access(distDir);
|
|
946
2510
|
} catch {
|
|
@@ -969,7 +2533,10 @@ var deploy = new Command3("deploy").description("Deploy your application to Clou
|
|
|
969
2533
|
}
|
|
970
2534
|
const uploadSignature = await generateUploadSignature(storeHash, accessToken, options.apiHost);
|
|
971
2535
|
await uploadBundleZip(uploadSignature.upload_url);
|
|
972
|
-
const
|
|
2536
|
+
const flagSecrets = parseEnvironmentVariables(options.secret) ?? [];
|
|
2537
|
+
const persistedSecrets = toDeploymentSecrets(getStoredEnv(config2));
|
|
2538
|
+
const mergedSecrets = mergeDeploymentSecrets(persistedSecrets, flagSecrets);
|
|
2539
|
+
const environmentVariables = mergedSecrets.length > 0 ? mergedSecrets : void 0;
|
|
973
2540
|
const { deployment_uuid: deploymentUuid } = await createDeployment(
|
|
974
2541
|
projectUuid,
|
|
975
2542
|
uploadSignature.upload_uuid,
|
|
@@ -978,41 +2545,109 @@ var deploy = new Command3("deploy").description("Deploy your application to Clou
|
|
|
978
2545
|
options.apiHost,
|
|
979
2546
|
environmentVariables
|
|
980
2547
|
);
|
|
981
|
-
await getDeploymentStatus(
|
|
2548
|
+
const deploymentHostname = await getDeploymentStatus(
|
|
2549
|
+
deploymentUuid,
|
|
2550
|
+
storeHash,
|
|
2551
|
+
accessToken,
|
|
2552
|
+
options.apiHost
|
|
2553
|
+
);
|
|
2554
|
+
if (!options.updateSiteUrl) {
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
try {
|
|
2558
|
+
await runChannelSiteUrlFlow({
|
|
2559
|
+
storeHash,
|
|
2560
|
+
accessToken,
|
|
2561
|
+
apiHost: options.apiHost,
|
|
2562
|
+
projectUuid,
|
|
2563
|
+
preferHostname: deploymentHostname
|
|
2564
|
+
});
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
consola.warn(
|
|
2567
|
+
`Failed to update channel site URL: ${error instanceof Error ? error.message : String(error)}`
|
|
2568
|
+
);
|
|
2569
|
+
consola.info(
|
|
2570
|
+
"Update it manually in the control panel, or re-run `catalyst auth login` if the token is missing the store_channel_settings scope."
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
982
2573
|
});
|
|
983
2574
|
|
|
984
|
-
// src/cli/commands/
|
|
985
|
-
import { Command as
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
).
|
|
995
|
-
var accessTokenOption = () => new Option4(
|
|
996
|
-
"--access-token <token>",
|
|
997
|
-
"BigCommerce access token. Can be found after creating a store-level API account."
|
|
998
|
-
).env("BIGCOMMERCE_ACCESS_TOKEN");
|
|
999
|
-
var apiHostOption = () => new Option4("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com");
|
|
1000
|
-
var projectUuidOption = () => new Option4(
|
|
1001
|
-
"--project-uuid <uuid>",
|
|
1002
|
-
"BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects)."
|
|
1003
|
-
).env("BIGCOMMERCE_PROJECT_UUID");
|
|
1004
|
-
var resolveProjectUuid = (options) => {
|
|
2575
|
+
// src/cli/commands/env.ts
|
|
2576
|
+
import { Command as Command6 } from "commander";
|
|
2577
|
+
var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2578
|
+
var add = new Command6("add").configureHelp({ showGlobalOptions: true }).description(
|
|
2579
|
+
"Add or update one or more deployment environment variables. Stored in .bigcommerce/project.json and sent as secrets on every `catalyst deploy`."
|
|
2580
|
+
).argument("<vars...>", "One or more environment variables in KEY=VALUE format.").addHelpText(
|
|
2581
|
+
"after",
|
|
2582
|
+
`
|
|
2583
|
+
Example:
|
|
2584
|
+
$ catalyst env add BIGCOMMERCE_STORE_HASH=abc123 BIGCOMMERCE_STOREFRONT_TOKEN=ey...`
|
|
2585
|
+
).action((vars) => {
|
|
1005
2586
|
const config2 = getProjectConfig();
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
2587
|
+
const stored = { ...getStoredEnv(config2) };
|
|
2588
|
+
const parsed = vars.map((entry) => parseEnvAssignment(entry));
|
|
2589
|
+
parsed.forEach(({ key, value }) => {
|
|
2590
|
+
stored[key] = value;
|
|
2591
|
+
});
|
|
2592
|
+
config2.set("env", stored);
|
|
2593
|
+
const keys = parsed.map(({ key }) => key);
|
|
2594
|
+
consola.success(
|
|
2595
|
+
`Saved ${keys.length} environment variable${keys.length === 1 ? "" : "s"} to .bigcommerce/project.json:`
|
|
2596
|
+
);
|
|
2597
|
+
keys.forEach((key) => consola.log(` ${key}=${MASK}`));
|
|
2598
|
+
process.exit(0);
|
|
2599
|
+
});
|
|
2600
|
+
var remove = new Command6("remove").configureHelp({ showGlobalOptions: true }).description("Remove one or more stored deployment environment variables.").argument("<keys...>", "One or more environment variable names to remove.").addHelpText(
|
|
2601
|
+
"after",
|
|
2602
|
+
`
|
|
2603
|
+
Example:
|
|
2604
|
+
$ catalyst env remove BIGCOMMERCE_STORE_HASH BIGCOMMERCE_STOREFRONT_TOKEN`
|
|
2605
|
+
).action((keys) => {
|
|
2606
|
+
const config2 = getProjectConfig();
|
|
2607
|
+
const stored = getStoredEnv(config2);
|
|
2608
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2609
|
+
keys.forEach((key) => {
|
|
2610
|
+
if (key in stored) {
|
|
2611
|
+
toRemove.add(key);
|
|
2612
|
+
} else {
|
|
2613
|
+
consola.warn(`No stored environment variable named "${key}". Skipping.`);
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
const next = Object.fromEntries(Object.entries(stored).filter(([key]) => !toRemove.has(key)));
|
|
2617
|
+
config2.set("env", next);
|
|
2618
|
+
const removed = Array.from(toRemove);
|
|
2619
|
+
if (removed.length > 0) {
|
|
2620
|
+
consola.success(
|
|
2621
|
+
`Removed ${removed.length} environment variable${removed.length === 1 ? "" : "s"}: ${removed.join(", ")}.`
|
|
1010
2622
|
);
|
|
1011
2623
|
}
|
|
1012
|
-
|
|
1013
|
-
};
|
|
2624
|
+
process.exit(0);
|
|
2625
|
+
});
|
|
2626
|
+
var list = new Command6("list").configureHelp({ showGlobalOptions: true }).description("List stored deployment environment variables (values are masked).").addHelpText(
|
|
2627
|
+
"after",
|
|
2628
|
+
`
|
|
2629
|
+
Example:
|
|
2630
|
+
$ catalyst env list`
|
|
2631
|
+
).action(() => {
|
|
2632
|
+
const config2 = getProjectConfig();
|
|
2633
|
+
const stored = getStoredEnv(config2);
|
|
2634
|
+
const keys = Object.keys(stored).sort();
|
|
2635
|
+
if (keys.length === 0) {
|
|
2636
|
+
consola.info("No environment variables stored. Add one with `catalyst env add KEY=VALUE`.");
|
|
2637
|
+
process.exit(0);
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
keys.forEach((key) => consola.log(`${key}=${MASK}`));
|
|
2641
|
+
process.exit(0);
|
|
2642
|
+
});
|
|
2643
|
+
var env = new Command6("env").configureHelp({ showGlobalOptions: true }).description(
|
|
2644
|
+
"Manage persistent deployment environment variables. These are sent as secrets on every `catalyst deploy`, so you no longer need to pass `--secret` each time."
|
|
2645
|
+
).addCommand(add).addCommand(remove).addCommand(list);
|
|
1014
2646
|
|
|
1015
2647
|
// src/cli/commands/logs.ts
|
|
2648
|
+
import { Command as Command7, Option as Option7 } from "commander";
|
|
2649
|
+
import { colorize as colorize5 } from "consola/utils";
|
|
2650
|
+
import { z as z11 } from "zod";
|
|
1016
2651
|
var telemetry = new Telemetry();
|
|
1017
2652
|
var DEFAULT_CONNECTION_TTL_MS = 1 * 60 * 1e3;
|
|
1018
2653
|
var MAX_RETRIES = 5;
|
|
@@ -1023,23 +2658,23 @@ var LEVEL_COLORS = {
|
|
|
1023
2658
|
ERROR: "red",
|
|
1024
2659
|
DEBUG: "gray"
|
|
1025
2660
|
};
|
|
1026
|
-
var LogEventSchema =
|
|
1027
|
-
uuid:
|
|
1028
|
-
project_uuid:
|
|
1029
|
-
request:
|
|
1030
|
-
method:
|
|
1031
|
-
url:
|
|
1032
|
-
status_code:
|
|
2661
|
+
var LogEventSchema = z11.object({
|
|
2662
|
+
uuid: z11.string(),
|
|
2663
|
+
project_uuid: z11.string(),
|
|
2664
|
+
request: z11.object({
|
|
2665
|
+
method: z11.string(),
|
|
2666
|
+
url: z11.string(),
|
|
2667
|
+
status_code: z11.number()
|
|
1033
2668
|
}),
|
|
1034
|
-
logs:
|
|
1035
|
-
|
|
1036
|
-
timestamp:
|
|
1037
|
-
level:
|
|
1038
|
-
messages:
|
|
2669
|
+
logs: z11.array(
|
|
2670
|
+
z11.object({
|
|
2671
|
+
timestamp: z11.string(),
|
|
2672
|
+
level: z11.string(),
|
|
2673
|
+
messages: z11.array(z11.unknown())
|
|
1039
2674
|
})
|
|
1040
2675
|
),
|
|
1041
|
-
exceptions:
|
|
1042
|
-
timestamp:
|
|
2676
|
+
exceptions: z11.array(z11.unknown()),
|
|
2677
|
+
timestamp: z11.string()
|
|
1043
2678
|
}).loose();
|
|
1044
2679
|
var StreamError = class extends Error {
|
|
1045
2680
|
constructor(message, fatal) {
|
|
@@ -1053,7 +2688,7 @@ var formatLogEvent = (event, format) => {
|
|
|
1053
2688
|
logEntries.forEach((entry) => {
|
|
1054
2689
|
const msg = formatMessages(entry.messages);
|
|
1055
2690
|
const level = entry.level.toUpperCase();
|
|
1056
|
-
const coloredLevel =
|
|
2691
|
+
const coloredLevel = colorize5(LEVEL_COLORS[level] ?? "white", level);
|
|
1057
2692
|
switch (format) {
|
|
1058
2693
|
case "short":
|
|
1059
2694
|
consola.log(msg);
|
|
@@ -1094,6 +2729,20 @@ var processLogEvent = (event, format) => {
|
|
|
1094
2729
|
consola.warn(`Failed to parse log event: ${event}`);
|
|
1095
2730
|
}
|
|
1096
2731
|
};
|
|
2732
|
+
var raceWithTimeout = async (promise, timeoutMs) => {
|
|
2733
|
+
let timeoutHandle;
|
|
2734
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
2735
|
+
timeoutHandle = setTimeout(() => resolve2({ kind: "timeout" }), timeoutMs);
|
|
2736
|
+
});
|
|
2737
|
+
try {
|
|
2738
|
+
return await Promise.race([
|
|
2739
|
+
promise.then((value) => ({ kind: "value", value })),
|
|
2740
|
+
timeoutPromise
|
|
2741
|
+
]);
|
|
2742
|
+
} finally {
|
|
2743
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
1097
2746
|
var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
1098
2747
|
const response = await fetch(
|
|
1099
2748
|
`https://${apiHost}/stores/${storeHash}/v3/infrastructure/logs/${projectUuid}/tail`,
|
|
@@ -1106,6 +2755,9 @@ var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
|
1106
2755
|
}
|
|
1107
2756
|
}
|
|
1108
2757
|
);
|
|
2758
|
+
if (response.status === 401) {
|
|
2759
|
+
throw new StreamError(new UnauthorizedError().message, true);
|
|
2760
|
+
}
|
|
1109
2761
|
if (!response.ok) {
|
|
1110
2762
|
throw new StreamError(
|
|
1111
2763
|
`Failed to open log stream: ${response.status} ${response.statusText}`,
|
|
@@ -1118,28 +2770,46 @@ var openLogStream = async (projectUuid, storeHash, accessToken, apiHost) => {
|
|
|
1118
2770
|
}
|
|
1119
2771
|
return reader;
|
|
1120
2772
|
};
|
|
2773
|
+
var pumpUntilRotation = async (reader, format, connectionTtlMs) => {
|
|
2774
|
+
const decoder = new TextDecoder();
|
|
2775
|
+
const connectTime = Date.now();
|
|
2776
|
+
let buffer = "";
|
|
2777
|
+
let receivedData = false;
|
|
2778
|
+
while (true) {
|
|
2779
|
+
const remainingTtlMs = connectionTtlMs - (Date.now() - connectTime);
|
|
2780
|
+
if (remainingTtlMs <= 0) {
|
|
2781
|
+
void reader.cancel();
|
|
2782
|
+
return "ttl";
|
|
2783
|
+
}
|
|
2784
|
+
const readResult = await raceWithTimeout(reader.read(), remainingTtlMs);
|
|
2785
|
+
if (readResult.kind === "timeout") {
|
|
2786
|
+
void reader.cancel();
|
|
2787
|
+
return receivedData ? "ttl" : "idle-timeout";
|
|
2788
|
+
}
|
|
2789
|
+
const { value, done: streamDone } = readResult.value;
|
|
2790
|
+
if (value) {
|
|
2791
|
+
receivedData = true;
|
|
2792
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2793
|
+
const parts = buffer.split("\n\n");
|
|
2794
|
+
buffer = parts.pop() ?? "";
|
|
2795
|
+
parts.map((raw) => parseSSEEvent(raw)).filter((event) => event !== null).forEach((event) => processLogEvent(event, format));
|
|
2796
|
+
}
|
|
2797
|
+
if (streamDone) {
|
|
2798
|
+
void reader.cancel();
|
|
2799
|
+
return "stream-done";
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
1121
2803
|
var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, connectionTtlMs = DEFAULT_CONNECTION_TTL_MS) => {
|
|
1122
2804
|
consola.info("Tailing logs...");
|
|
1123
2805
|
let retries = 0;
|
|
1124
2806
|
while (true) {
|
|
1125
2807
|
try {
|
|
1126
2808
|
const reader = await openLogStream(projectUuid, storeHash, accessToken, apiHost);
|
|
1127
|
-
const decoder = new TextDecoder();
|
|
1128
|
-
const connectTime = Date.now();
|
|
1129
|
-
let buffer = "";
|
|
1130
2809
|
retries = 0;
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1135
|
-
const parts = buffer.split("\n\n");
|
|
1136
|
-
buffer = parts.pop() ?? "";
|
|
1137
|
-
parts.map((raw) => parseSSEEvent(raw)).filter((event) => event !== null).forEach((event) => processLogEvent(event, format));
|
|
1138
|
-
}
|
|
1139
|
-
if (streamDone || Date.now() - connectTime >= connectionTtlMs) {
|
|
1140
|
-
void reader.cancel();
|
|
1141
|
-
break;
|
|
1142
|
-
}
|
|
2810
|
+
const rotation = await pumpUntilRotation(reader, format, connectionTtlMs);
|
|
2811
|
+
if (rotation === "idle-timeout") {
|
|
2812
|
+
consola.warn("Log stream idle, reconnecting...");
|
|
1143
2813
|
}
|
|
1144
2814
|
} catch (error) {
|
|
1145
2815
|
if (error instanceof StreamError && error.fatal) {
|
|
@@ -1161,45 +2831,74 @@ var tailLogs = async (projectUuid, storeHash, accessToken, apiHost, format, conn
|
|
|
1161
2831
|
}
|
|
1162
2832
|
}
|
|
1163
2833
|
};
|
|
1164
|
-
var tail = new
|
|
1165
|
-
|
|
2834
|
+
var tail = new Command7("tail").configureHelp({ showGlobalOptions: true }).description("Tail live logs from your deployed application.").addHelpText(
|
|
2835
|
+
"after",
|
|
2836
|
+
`
|
|
2837
|
+
Examples:
|
|
2838
|
+
$ catalyst logs tail
|
|
2839
|
+
|
|
2840
|
+
# Tail logs with request format
|
|
2841
|
+
$ catalyst logs tail --format request
|
|
2842
|
+
|
|
2843
|
+
# Tail logs as raw JSON (useful for piping to other tools)
|
|
2844
|
+
$ catalyst logs tail --format json`
|
|
2845
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).addOption(
|
|
2846
|
+
new Option7("--format <format>", "Output format for log events.").choices(["json", "pretty", "default", "short", "request"]).default("default")
|
|
1166
2847
|
).action(async (options) => {
|
|
1167
2848
|
try {
|
|
1168
|
-
|
|
2849
|
+
const config2 = getProjectConfig();
|
|
2850
|
+
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
2851
|
+
await telemetry.identify(storeHash);
|
|
1169
2852
|
const projectUuid = resolveProjectUuid(options);
|
|
1170
|
-
await tailLogs(
|
|
1171
|
-
projectUuid,
|
|
1172
|
-
options.storeHash,
|
|
1173
|
-
options.accessToken,
|
|
1174
|
-
options.apiHost,
|
|
1175
|
-
options.format
|
|
1176
|
-
);
|
|
2853
|
+
await tailLogs(projectUuid, storeHash, accessToken, options.apiHost, options.format);
|
|
1177
2854
|
} catch (error) {
|
|
1178
2855
|
consola.error(error);
|
|
1179
2856
|
process.exit(1);
|
|
1180
2857
|
}
|
|
1181
2858
|
});
|
|
1182
|
-
var query = new
|
|
2859
|
+
var query = new Command7("query").configureHelp({ showGlobalOptions: true }).description("Query historical logs from your deployed application.").addHelpText(
|
|
2860
|
+
"after",
|
|
2861
|
+
`
|
|
2862
|
+
Example:
|
|
2863
|
+
$ catalyst logs query`
|
|
2864
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).action((options) => {
|
|
2865
|
+
const config2 = getProjectConfig();
|
|
2866
|
+
resolveCredentials(options, config2);
|
|
1183
2867
|
consola.error("The query command is not yet implemented.");
|
|
1184
2868
|
process.exit(1);
|
|
1185
2869
|
});
|
|
1186
|
-
var logs = new
|
|
2870
|
+
var logs = new Command7("logs").configureHelp({ showGlobalOptions: true }).description("View logs from your deployed application.").addCommand(tail, { isDefault: true }).addCommand(query);
|
|
1187
2871
|
|
|
1188
2872
|
// src/cli/commands/project.ts
|
|
1189
|
-
import { Command as
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
).
|
|
1195
|
-
)
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
2873
|
+
import { Command as Command8 } from "commander";
|
|
2874
|
+
import { colorize as colorize6 } from "consola/utils";
|
|
2875
|
+
import { dirname as dirname5 } from "path";
|
|
2876
|
+
async function offerCommerceHostingSetup(projectUuid, credentials) {
|
|
2877
|
+
const projectDir = dirname5(process.cwd());
|
|
2878
|
+
if (getProjectState().isTransformed) {
|
|
2879
|
+
await cleanupCloudflareIncompatibilities(projectDir);
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const shouldSetup = await consola.prompt(
|
|
2883
|
+
"Your project has been linked, but is not fully set up for Commerce Hosting deployments yet. Would you like to run the setup now?",
|
|
2884
|
+
{ type: "confirm", initial: true }
|
|
2885
|
+
);
|
|
2886
|
+
if (!shouldSetup) return;
|
|
2887
|
+
await setupCommerceHosting({
|
|
2888
|
+
projectDir,
|
|
2889
|
+
projectUuid,
|
|
2890
|
+
storeHash: credentials?.storeHash,
|
|
2891
|
+
accessToken: credentials?.accessToken
|
|
2892
|
+
});
|
|
2893
|
+
consola.success("Commerce Hosting setup complete.");
|
|
2894
|
+
await installDependencies(projectDir);
|
|
2895
|
+
}
|
|
2896
|
+
var list2 = new Command8("list").configureHelp({ showGlobalOptions: true }).description("List BigCommerce infrastructure projects for your store.").addHelpText(
|
|
2897
|
+
"after",
|
|
2898
|
+
`
|
|
2899
|
+
Example:
|
|
2900
|
+
$ catalyst project list`
|
|
2901
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).action(async (options) => {
|
|
1203
2902
|
const config2 = getProjectConfig();
|
|
1204
2903
|
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
1205
2904
|
await getTelemetry().identify(storeHash);
|
|
@@ -1211,28 +2910,54 @@ var list = new Command5("list").description("List BigCommerce infrastructure pro
|
|
|
1211
2910
|
process.exit(0);
|
|
1212
2911
|
return;
|
|
1213
2912
|
}
|
|
2913
|
+
const linkedProjectUuid = config2.get("projectUuid");
|
|
1214
2914
|
projects.forEach((p) => {
|
|
1215
|
-
|
|
2915
|
+
const marker = p.uuid === linkedProjectUuid ? ` ${colorize6("green", "[linked]")}` : "";
|
|
2916
|
+
consola.log(`${p.name} (${p.uuid})${marker}`);
|
|
2917
|
+
if (p.deployment_hostnames.length === 0) {
|
|
2918
|
+
consola.log(" (not deployed)");
|
|
2919
|
+
} else {
|
|
2920
|
+
p.deployment_hostnames.forEach((hostname) => {
|
|
2921
|
+
consola.log(` ${colorize6("blue", `https://${hostname}`)}`);
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
consola.log("");
|
|
1216
2925
|
});
|
|
1217
2926
|
process.exit(0);
|
|
1218
2927
|
});
|
|
1219
|
-
var
|
|
1220
|
-
"Create a new BigCommerce infrastructure project and link it to your local Catalyst project."
|
|
1221
|
-
).
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
).addOption(
|
|
1227
|
-
new Option6(
|
|
1228
|
-
"--access-token <token>",
|
|
1229
|
-
"BigCommerce access token. Can be found after creating a store-level API account."
|
|
1230
|
-
).env("CATALYST_ACCESS_TOKEN")
|
|
1231
|
-
).addOption(
|
|
1232
|
-
new Option6("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com")
|
|
1233
|
-
).action(async (options) => {
|
|
2928
|
+
var create2 = new Command8("create").configureHelp({ showGlobalOptions: true }).description(
|
|
2929
|
+
"Create a new BigCommerce infrastructure project and link it to your local Catalyst project. Walks you through logging in if you are not authenticated yet."
|
|
2930
|
+
).addHelpText(
|
|
2931
|
+
"after",
|
|
2932
|
+
`
|
|
2933
|
+
Example:
|
|
2934
|
+
$ catalyst project create`
|
|
2935
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(loginUrlOption()).action(async (options) => {
|
|
1234
2936
|
const config2 = getProjectConfig();
|
|
1235
|
-
|
|
2937
|
+
let storeHash = options.storeHash ?? config2.get("storeHash");
|
|
2938
|
+
let accessToken = options.accessToken ?? config2.get("accessToken");
|
|
2939
|
+
if (!storeHash || !accessToken) {
|
|
2940
|
+
consola.info(
|
|
2941
|
+
"You're not logged in yet. Let's get you authenticated before creating a project."
|
|
2942
|
+
);
|
|
2943
|
+
try {
|
|
2944
|
+
const credentials = await login(options.loginUrl, options.apiHost);
|
|
2945
|
+
storeHash = credentials.storeHash;
|
|
2946
|
+
accessToken = credentials.accessToken;
|
|
2947
|
+
config2.set("storeHash", storeHash);
|
|
2948
|
+
config2.set("accessToken", accessToken);
|
|
2949
|
+
consola.success(`Logged in to store ${storeHash}.`);
|
|
2950
|
+
} catch (error) {
|
|
2951
|
+
if (error instanceof LoginAbortedError) {
|
|
2952
|
+
consola.info(
|
|
2953
|
+
"Login aborted. Re-run `catalyst project create` when you have your credentials ready."
|
|
2954
|
+
);
|
|
2955
|
+
process.exit(0);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
throw error;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
1236
2961
|
await getTelemetry().identify(storeHash);
|
|
1237
2962
|
const newProjectName = await consola.prompt("Enter a name for the new project:", {
|
|
1238
2963
|
type: "text"
|
|
@@ -1246,24 +2971,18 @@ var create = new Command5("create").description(
|
|
|
1246
2971
|
consola.success("Project UUID written to .bigcommerce/project.json.");
|
|
1247
2972
|
process.exit(0);
|
|
1248
2973
|
});
|
|
1249
|
-
var link = new
|
|
2974
|
+
var link = new Command8("link").configureHelp({ showGlobalOptions: true }).description(
|
|
1250
2975
|
"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."
|
|
1251
|
-
).
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
).addOption(
|
|
1262
|
-
new Option6("--api-host <host>", "BigCommerce API host. The default is api.bigcommerce.com.").env("BIGCOMMERCE_API_HOST").default("api.bigcommerce.com")
|
|
1263
|
-
).option(
|
|
1264
|
-
"--project-uuid <uuid>",
|
|
1265
|
-
"BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Use this to link directly without fetching projects."
|
|
1266
|
-
).action(async (options) => {
|
|
2976
|
+
).addHelpText(
|
|
2977
|
+
"after",
|
|
2978
|
+
`
|
|
2979
|
+
Examples:
|
|
2980
|
+
# Link interactively (prompts to select or create)
|
|
2981
|
+
$ catalyst project link
|
|
2982
|
+
|
|
2983
|
+
# Link using a project UUID directly
|
|
2984
|
+
$ catalyst project link --project-uuid <UUID>`
|
|
2985
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).action(async (options) => {
|
|
1267
2986
|
const config2 = getProjectConfig();
|
|
1268
2987
|
const writeProjectConfig = (uuid, credentials) => {
|
|
1269
2988
|
consola.start("Writing project UUID to .bigcommerce/project.json...");
|
|
@@ -1276,63 +2995,142 @@ var link = new Command5("link").description(
|
|
|
1276
2995
|
};
|
|
1277
2996
|
if (options.projectUuid) {
|
|
1278
2997
|
writeProjectConfig(options.projectUuid);
|
|
2998
|
+
await offerCommerceHostingSetup(options.projectUuid);
|
|
1279
2999
|
process.exit(0);
|
|
1280
3000
|
return;
|
|
1281
3001
|
}
|
|
1282
3002
|
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
1283
3003
|
await getTelemetry().identify(storeHash);
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
3004
|
+
let selected;
|
|
3005
|
+
try {
|
|
3006
|
+
selected = await selectOrCreateInfrastructureProject(
|
|
3007
|
+
{ storeHash, accessToken, apiHost: options.apiHost },
|
|
3008
|
+
config2.get("projectUuid")
|
|
3009
|
+
);
|
|
3010
|
+
} catch (error) {
|
|
3011
|
+
if (error instanceof NoLinkedProjectError) {
|
|
3012
|
+
consola.info(
|
|
3013
|
+
"When you're ready to create a project, run `catalyst project create` or re-run `catalyst project link`."
|
|
3014
|
+
);
|
|
3015
|
+
process.exit(0);
|
|
3016
|
+
throw error;
|
|
1297
3017
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
3018
|
+
throw error;
|
|
3019
|
+
}
|
|
3020
|
+
writeProjectConfig(selected.uuid, { storeHash, accessToken });
|
|
3021
|
+
await offerCommerceHostingSetup(selected.uuid, { storeHash, accessToken });
|
|
3022
|
+
process.exit(0);
|
|
3023
|
+
});
|
|
3024
|
+
var del = new Command8("delete").configureHelp({ showGlobalOptions: true }).description(
|
|
3025
|
+
"Permanently delete a BigCommerce infrastructure project. This action is irreversible."
|
|
3026
|
+
).addHelpText(
|
|
3027
|
+
"after",
|
|
3028
|
+
`
|
|
3029
|
+
Examples:
|
|
3030
|
+
# Select a project to delete interactively
|
|
3031
|
+
$ catalyst project delete
|
|
3032
|
+
|
|
3033
|
+
# Delete a specific project (still prompts for confirmation)
|
|
3034
|
+
$ catalyst project delete --project-uuid <UUID>
|
|
3035
|
+
|
|
3036
|
+
# Skip the confirmation prompt
|
|
3037
|
+
$ catalyst project delete --project-uuid <UUID> --force`
|
|
3038
|
+
).addOption(storeHashOption()).addOption(accessTokenOption()).addOption(apiHostOption()).addOption(projectUuidOption()).option("--force", "Skip the confirmation prompt before deleting.").action(async (options) => {
|
|
3039
|
+
const config2 = getProjectConfig();
|
|
3040
|
+
const { storeHash, accessToken } = resolveCredentials(options, config2);
|
|
3041
|
+
await getTelemetry().identify(storeHash);
|
|
3042
|
+
let targetUuid = options.projectUuid;
|
|
3043
|
+
let targetName;
|
|
3044
|
+
if (!targetUuid) {
|
|
3045
|
+
consola.start("Fetching projects...");
|
|
3046
|
+
const projects = await fetchProjects(storeHash, accessToken, options.apiHost);
|
|
3047
|
+
consola.success("Projects fetched.");
|
|
3048
|
+
if (projects.length === 0) {
|
|
3049
|
+
consola.info("No projects found.");
|
|
3050
|
+
process.exit(0);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
const linkedProjectUuid = config2.get("projectUuid");
|
|
3054
|
+
const selected = await consola.prompt(
|
|
3055
|
+
"Select a project to delete (Press <enter> to select).",
|
|
3056
|
+
{
|
|
3057
|
+
type: "select",
|
|
3058
|
+
options: [
|
|
3059
|
+
...projects.map((p) => ({
|
|
3060
|
+
label: p.uuid === linkedProjectUuid ? `${p.name} ${colorize6("green", "[linked]")}` : p.name,
|
|
3061
|
+
value: p.uuid,
|
|
3062
|
+
hint: p.uuid
|
|
3063
|
+
})),
|
|
3064
|
+
{ label: "Cancel", value: "cancel", hint: "Exit without deleting any project." }
|
|
3065
|
+
],
|
|
3066
|
+
cancel: "reject"
|
|
3067
|
+
}
|
|
3068
|
+
);
|
|
3069
|
+
if (selected === "cancel") {
|
|
3070
|
+
consola.info("Aborted. No project was deleted.");
|
|
3071
|
+
process.exit(0);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const matched = projects.find((p) => p.uuid === selected);
|
|
3075
|
+
if (!matched) {
|
|
3076
|
+
throw new Error(`Selected project ${String(selected)} not found in fetched list.`);
|
|
3077
|
+
}
|
|
3078
|
+
targetUuid = matched.uuid;
|
|
3079
|
+
targetName = matched.name;
|
|
3080
|
+
}
|
|
3081
|
+
if (!options.force) {
|
|
3082
|
+
const label = targetName ? `"${targetName}" (${targetUuid})` : targetUuid;
|
|
3083
|
+
const confirmed = await consola.prompt(
|
|
3084
|
+
`Are you sure you want to delete project ${label}? This action is irreversible and will permanently destroy the project and all of its data.`,
|
|
3085
|
+
{ type: "confirm", initial: false }
|
|
3086
|
+
);
|
|
3087
|
+
if (!confirmed) {
|
|
3088
|
+
consola.info("Aborted. No project was deleted.");
|
|
3089
|
+
process.exit(0);
|
|
3090
|
+
return;
|
|
1305
3091
|
}
|
|
1306
|
-
);
|
|
1307
|
-
if (projectUuid === "create") {
|
|
1308
|
-
const newProjectName = await consola.prompt("Enter a name for the new project:", {
|
|
1309
|
-
type: "text"
|
|
1310
|
-
});
|
|
1311
|
-
const data = await createProject(newProjectName, storeHash, accessToken, options.apiHost);
|
|
1312
|
-
projectUuid = data.uuid;
|
|
1313
|
-
consola.success(`Project "${data.name}" created successfully.`);
|
|
1314
3092
|
}
|
|
1315
|
-
|
|
3093
|
+
consola.start(`Deleting project ${targetUuid}...`);
|
|
3094
|
+
await deleteProject(targetUuid, storeHash, accessToken, options.apiHost);
|
|
3095
|
+
consola.success(`Project ${targetUuid} deleted.`);
|
|
3096
|
+
if (config2.get("projectUuid") === targetUuid) {
|
|
3097
|
+
config2.delete("projectUuid");
|
|
3098
|
+
consola.info("Removed project UUID from .bigcommerce/project.json.");
|
|
3099
|
+
}
|
|
1316
3100
|
process.exit(0);
|
|
1317
3101
|
});
|
|
1318
|
-
var project = new
|
|
3102
|
+
var project = new Command8("project").configureHelp({ showGlobalOptions: true }).description("Manage your BigCommerce infrastructure project.").addCommand(create2).addCommand(list2).addCommand(link).addCommand(del);
|
|
1319
3103
|
|
|
1320
3104
|
// src/cli/commands/start.ts
|
|
1321
|
-
import { Command as
|
|
3105
|
+
import { Command as Command9 } from "commander";
|
|
1322
3106
|
import { execa as execa2 } from "execa";
|
|
1323
|
-
import { existsSync, lstatSync, symlinkSync } from "fs";
|
|
1324
|
-
import { join as
|
|
1325
|
-
var start = new
|
|
3107
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, symlinkSync as symlinkSync2 } from "fs";
|
|
3108
|
+
import { join as join11, relative } from "path";
|
|
3109
|
+
var start = new Command9("start").configureHelp({ showGlobalOptions: true }).description(
|
|
1326
3110
|
"Start a local preview of your Catalyst storefront using the OpenNext Cloudflare adapter."
|
|
3111
|
+
).addHelpText(
|
|
3112
|
+
"after",
|
|
3113
|
+
`
|
|
3114
|
+
Example:
|
|
3115
|
+
$ catalyst start`
|
|
1327
3116
|
).action(async () => {
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
3117
|
+
const state = getProjectState();
|
|
3118
|
+
if (!state.isTransformed) {
|
|
3119
|
+
consola.info("Project is not set up for Commerce Hosting \u2014 running `next start`.");
|
|
3120
|
+
await execa2("pnpm", ["exec", "next", "start"], {
|
|
3121
|
+
stdio: "inherit",
|
|
3122
|
+
cwd: process.cwd()
|
|
3123
|
+
});
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
const envLocal = join11(process.cwd(), ".env.local");
|
|
3127
|
+
const devVars = join11(process.cwd(), ".bigcommerce", ".dev.vars");
|
|
3128
|
+
if (existsSync4(envLocal)) {
|
|
3129
|
+
if (!existsSync4(devVars)) {
|
|
3130
|
+
const target = relative(join11(process.cwd(), ".bigcommerce"), envLocal);
|
|
3131
|
+
symlinkSync2(target, devVars);
|
|
1334
3132
|
consola.success("Symlinked .bigcommerce/.dev.vars \u2192 .env.local");
|
|
1335
|
-
} else if (!
|
|
3133
|
+
} else if (!lstatSync2(devVars).isSymbolicLink()) {
|
|
1336
3134
|
consola.warn(
|
|
1337
3135
|
".bigcommerce/.dev.vars already exists and is not a symlink. Wrangler will use it instead of .env.local."
|
|
1338
3136
|
);
|
|
@@ -1349,7 +3147,7 @@ var start = new Command6("start").description(
|
|
|
1349
3147
|
"opennextjs-cloudflare",
|
|
1350
3148
|
"preview",
|
|
1351
3149
|
"--config",
|
|
1352
|
-
|
|
3150
|
+
join11(".bigcommerce", "wrangler.jsonc")
|
|
1353
3151
|
],
|
|
1354
3152
|
{
|
|
1355
3153
|
stdio: "inherit",
|
|
@@ -1359,11 +3157,25 @@ var start = new Command6("start").description(
|
|
|
1359
3157
|
});
|
|
1360
3158
|
|
|
1361
3159
|
// src/cli/commands/telemetry.ts
|
|
1362
|
-
import { Argument, Command as
|
|
1363
|
-
import { colorize as
|
|
3160
|
+
import { Argument, Command as Command10, Option as Option8 } from "commander";
|
|
3161
|
+
import { colorize as colorize7 } from "consola/utils";
|
|
1364
3162
|
var telemetryService = getTelemetry();
|
|
1365
3163
|
var isEnabled = telemetryService.isEnabled();
|
|
1366
|
-
var telemetry2 = new
|
|
3164
|
+
var telemetry2 = new Command10("telemetry").configureHelp({ showGlobalOptions: true }).description(
|
|
3165
|
+
"View or change CLI telemetry collection status. Enabling telemetry helps BigCommerce support diagnose and troubleshoot errors you encounter when using the CLI."
|
|
3166
|
+
).addHelpText(
|
|
3167
|
+
"after",
|
|
3168
|
+
`
|
|
3169
|
+
Examples:
|
|
3170
|
+
# Show telemetry status
|
|
3171
|
+
$ catalyst telemetry
|
|
3172
|
+
|
|
3173
|
+
# Enable telemetry collection
|
|
3174
|
+
$ catalyst telemetry enable
|
|
3175
|
+
|
|
3176
|
+
# Disable telemetry collection
|
|
3177
|
+
$ catalyst telemetry --disable`
|
|
3178
|
+
).addArgument(new Argument("[arg]").choices(["disable", "enable", "status"])).addOption(new Option8("--enable", `Enables CLI telemetry collection.`).conflicts("disable")).option("--disable", `Disables CLI telemetry collection.`).action((arg, options) => {
|
|
1367
3179
|
if (options.enable || arg === "enable") {
|
|
1368
3180
|
telemetryService.setEnabled(true);
|
|
1369
3181
|
isEnabled = true;
|
|
@@ -1371,7 +3183,7 @@ var telemetry2 = new Command7("telemetry").addArgument(new Argument("[arg]").cho
|
|
|
1371
3183
|
} else if (options.disable || arg === "disable") {
|
|
1372
3184
|
telemetryService.setEnabled(false);
|
|
1373
3185
|
if (isEnabled) {
|
|
1374
|
-
consola.success("Your preference has been saved
|
|
3186
|
+
consola.success("Your preference has been saved.");
|
|
1375
3187
|
} else {
|
|
1376
3188
|
consola.info(`Catalyst CLI telemetry collection is already disabled.`);
|
|
1377
3189
|
}
|
|
@@ -1380,7 +3192,7 @@ var telemetry2 = new Command7("telemetry").addArgument(new Argument("[arg]").cho
|
|
|
1380
3192
|
consola.info("Catalyst CLI Telemetry\n");
|
|
1381
3193
|
}
|
|
1382
3194
|
consola.info(
|
|
1383
|
-
`Status: ${
|
|
3195
|
+
`Status: ${colorize7("bold", isEnabled ? colorize7("green", "Enabled") : colorize7("red", "Disabled"))}`
|
|
1384
3196
|
);
|
|
1385
3197
|
if (!isEnabled) {
|
|
1386
3198
|
consola.info(
|
|
@@ -1390,8 +3202,13 @@ var telemetry2 = new Command7("telemetry").addArgument(new Argument("[arg]").cho
|
|
|
1390
3202
|
});
|
|
1391
3203
|
|
|
1392
3204
|
// src/cli/commands/version.ts
|
|
1393
|
-
import { Command as
|
|
1394
|
-
var version = new
|
|
3205
|
+
import { Command as Command11 } from "commander";
|
|
3206
|
+
var version = new Command11("version").configureHelp({ showGlobalOptions: true }).description("Display detailed version information.").addHelpText(
|
|
3207
|
+
"after",
|
|
3208
|
+
`
|
|
3209
|
+
Example:
|
|
3210
|
+
$ catalyst version`
|
|
3211
|
+
).action(() => {
|
|
1395
3212
|
consola.log("Version Information:");
|
|
1396
3213
|
consola.log(`CLI Version: ${package_default.version}`);
|
|
1397
3214
|
consola.log(`Node Version: ${process.version}`);
|
|
@@ -1447,11 +3264,20 @@ var telemetryPostHook = async () => {
|
|
|
1447
3264
|
};
|
|
1448
3265
|
|
|
1449
3266
|
// src/cli/program.ts
|
|
1450
|
-
var
|
|
1451
|
-
|
|
3267
|
+
var defaultEnvPath = resolve(process.cwd(), ".env.local");
|
|
3268
|
+
if (existsSync5(defaultEnvPath)) {
|
|
3269
|
+
config({ path: defaultEnvPath });
|
|
3270
|
+
}
|
|
3271
|
+
if (!process.env.CATALYST_STORE_HASH && process.env.BIGCOMMERCE_STORE_HASH) {
|
|
3272
|
+
process.env.CATALYST_STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;
|
|
3273
|
+
}
|
|
3274
|
+
var program = new Command12();
|
|
3275
|
+
consola.log(colorize8("cyanBright", `\u25E2 ${package_default.name} v${package_default.version}
|
|
1452
3276
|
`));
|
|
1453
|
-
program.name(package_default.name).version(package_default.version).
|
|
1454
|
-
|
|
3277
|
+
program.name(package_default.name).version(package_default.version).summary("CLI tool for Catalyst development").description(
|
|
3278
|
+
"CLI tool for Catalyst development.\n\nConfiguration priority: flags > env file (--env-path) > process.env > .env.local (auto-loaded from cwd) > .bigcommerce/project.json.\n\nCATALYST_STORE_HASH falls back to BIGCOMMERCE_STORE_HASH if unset.\n\nRun `catalyst <command> --help` for details on a specific command."
|
|
3279
|
+
).configureHelp({ showGlobalOptions: true }).addOption(
|
|
3280
|
+
new Option9(
|
|
1455
3281
|
"--env-path <path>",
|
|
1456
3282
|
"Path to environment file to load (relative to current working directory)"
|
|
1457
3283
|
// We are using argParser, because commander loads in environment variables before executing hooks.
|
|
@@ -1467,12 +3293,12 @@ program.name(package_default.name).version(package_default.version).description(
|
|
|
1467
3293
|
const message = errCode === "ENOENT" ? `Env file not found: ${envFilePath}` : `Failed to load --env-path ${value}: ${result.error.message}`;
|
|
1468
3294
|
throw new Error(message);
|
|
1469
3295
|
}
|
|
1470
|
-
consola.log(
|
|
3296
|
+
consola.log(colorize8("cyanBright", `Loaded environment variables from ${envFilePath}
|
|
1471
3297
|
`));
|
|
1472
3298
|
}
|
|
1473
3299
|
return value;
|
|
1474
3300
|
})
|
|
1475
|
-
).addCommand(version).addCommand(start).addCommand(build).addCommand(deploy).addCommand(logs).addCommand(project).addCommand(auth).addCommand(telemetry2).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
|
|
3301
|
+
).addCommand(version).addCommand(create).addCommand(start).addCommand(build).addCommand(deploy).addCommand(logs).addCommand(project).addCommand(env).addCommand(channel).addCommand(auth).addCommand(telemetry2).hook("preAction", telemetryPreHook).hook("postAction", telemetryPostHook);
|
|
1476
3302
|
|
|
1477
3303
|
// src/cli/index.ts
|
|
1478
3304
|
var handleFatalError = async (error) => {
|
|
@@ -1489,6 +3315,10 @@ var handleFatalError = async (error) => {
|
|
|
1489
3315
|
await telemetry3.analytics.closeAndFlush();
|
|
1490
3316
|
} catch {
|
|
1491
3317
|
}
|
|
3318
|
+
if (error instanceof UnauthorizedError) {
|
|
3319
|
+
consola.error(errorMessage);
|
|
3320
|
+
process.exit(1);
|
|
3321
|
+
}
|
|
1492
3322
|
consola.error(errorMessage);
|
|
1493
3323
|
if (telemetry3.isEnabled()) {
|
|
1494
3324
|
consola.info(
|