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