@strapi/cloud-cli 4.25.3 → 4.25.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,7 +29,6 @@ const chalk = require("chalk");
29
29
  const axios = require("axios");
30
30
  const crypto = require("node:crypto");
31
31
  const utils = require("@strapi/utils");
32
- const fs = require("fs");
33
32
  const tar = require("tar");
34
33
  const minimatch = require("minimatch");
35
34
  const inquirer = require("inquirer");
@@ -41,7 +40,6 @@ const jwt = require("jsonwebtoken");
41
40
  const stringify = require("fast-safe-stringify");
42
41
  const ora = require("ora");
43
42
  const cliProgress = require("cli-progress");
44
- const fs$1 = require("fs/promises");
45
43
  const pkgUp = require("pkg-up");
46
44
  const yup = require("yup");
47
45
  const _ = require("lodash");
@@ -66,12 +64,11 @@ function _interopNamespace(e) {
66
64
  return Object.freeze(n);
67
65
  }
68
66
  const crypto__default = /* @__PURE__ */ _interopDefault(crypto$1);
69
- const fse__default = /* @__PURE__ */ _interopDefault(fse);
67
+ const fse__namespace = /* @__PURE__ */ _interopNamespace(fse);
70
68
  const path__namespace = /* @__PURE__ */ _interopNamespace(path);
71
69
  const chalk__default = /* @__PURE__ */ _interopDefault(chalk);
72
70
  const axios__default = /* @__PURE__ */ _interopDefault(axios);
73
71
  const crypto__namespace = /* @__PURE__ */ _interopNamespace(crypto);
74
- const fs__namespace = /* @__PURE__ */ _interopNamespace(fs);
75
72
  const tar__namespace = /* @__PURE__ */ _interopNamespace(tar);
76
73
  const inquirer__default = /* @__PURE__ */ _interopDefault(inquirer);
77
74
  const os__default = /* @__PURE__ */ _interopDefault(os);
@@ -81,7 +78,6 @@ const jwt__default = /* @__PURE__ */ _interopDefault(jwt);
81
78
  const stringify__default = /* @__PURE__ */ _interopDefault(stringify);
82
79
  const ora__default = /* @__PURE__ */ _interopDefault(ora);
83
80
  const cliProgress__namespace = /* @__PURE__ */ _interopNamespace(cliProgress);
84
- const fs__default = /* @__PURE__ */ _interopDefault(fs$1);
85
81
  const pkgUp__default = /* @__PURE__ */ _interopDefault(pkgUp);
86
82
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
87
83
  const ___default = /* @__PURE__ */ _interopDefault(_);
@@ -104,23 +100,6 @@ const IGNORED_PATTERNS = [
104
100
  "**/.idea/**",
105
101
  "**/.vscode/**"
106
102
  ];
107
- const getFiles = (dirPath, ignorePatterns = [], arrayOfFiles = [], subfolder = "") => {
108
- const entries = fs__namespace.readdirSync(path__namespace.join(dirPath, subfolder));
109
- entries.forEach((entry) => {
110
- const entryPathFromRoot = path__namespace.join(subfolder, entry);
111
- const entryPath = path__namespace.relative(dirPath, entryPathFromRoot);
112
- const isIgnored = isIgnoredFile(dirPath, entryPathFromRoot, ignorePatterns);
113
- if (isIgnored) {
114
- return;
115
- }
116
- if (fs__namespace.statSync(entryPath).isDirectory()) {
117
- getFiles(dirPath, ignorePatterns, arrayOfFiles, entryPathFromRoot);
118
- } else {
119
- arrayOfFiles.push(entryPath);
120
- }
121
- });
122
- return arrayOfFiles;
123
- };
124
103
  const isIgnoredFile = (folderPath, file, ignorePatterns) => {
125
104
  ignorePatterns.push(...IGNORED_PATTERNS);
126
105
  const relativeFilePath = path__namespace.join(folderPath, file);
@@ -138,16 +117,35 @@ const isIgnoredFile = (folderPath, file, ignorePatterns) => {
138
117
  }
139
118
  return isIgnored;
140
119
  };
141
- const readGitignore = (folderPath) => {
120
+ const getFiles = async (dirPath, ignorePatterns = [], subfolder = "") => {
121
+ const arrayOfFiles = [];
122
+ const entries = await fse__namespace.readdir(path__namespace.join(dirPath, subfolder));
123
+ for (const entry of entries) {
124
+ const entryPathFromRoot = path__namespace.join(subfolder, entry);
125
+ const entryPath = path__namespace.relative(dirPath, entryPathFromRoot);
126
+ const isIgnored = isIgnoredFile(dirPath, entryPathFromRoot, ignorePatterns);
127
+ if (!isIgnored) {
128
+ if (fse__namespace.statSync(entryPath).isDirectory()) {
129
+ const subFiles = await getFiles(dirPath, ignorePatterns, entryPathFromRoot);
130
+ arrayOfFiles.push(...subFiles);
131
+ } else {
132
+ arrayOfFiles.push(entryPath);
133
+ }
134
+ }
135
+ }
136
+ return arrayOfFiles;
137
+ };
138
+ const readGitignore = async (folderPath) => {
142
139
  const gitignorePath = path__namespace.resolve(folderPath, ".gitignore");
143
- if (!fs__namespace.existsSync(gitignorePath))
140
+ const pathExist = await fse__namespace.pathExists(gitignorePath);
141
+ if (!pathExist)
144
142
  return [];
145
- const gitignoreContent = fs__namespace.readFileSync(gitignorePath, "utf8");
143
+ const gitignoreContent = await fse__namespace.readFile(gitignorePath, "utf8");
146
144
  return gitignoreContent.split(/\r?\n/).filter((line) => Boolean(line.trim()) && !line.startsWith("#"));
147
145
  };
148
146
  const compressFilesToTar = async (storagePath, folderToCompress, filename) => {
149
- const ignorePatterns = readGitignore(folderToCompress);
150
- const filesToCompress = getFiles(folderToCompress, ignorePatterns);
147
+ const ignorePatterns = await readGitignore(folderToCompress);
148
+ const filesToCompress = await getFiles(folderToCompress, ignorePatterns);
151
149
  return tar__namespace.c(
152
150
  {
153
151
  gzip: true,
@@ -160,7 +158,7 @@ const APP_FOLDER_NAME = "com.strapi.cli";
160
158
  const CONFIG_FILENAME = "config.json";
161
159
  async function checkDirectoryExists(directoryPath) {
162
160
  try {
163
- const fsStat = await fse__default.default.lstat(directoryPath);
161
+ const fsStat = await fse__namespace.default.lstat(directoryPath);
164
162
  return fsStat.isDirectory();
165
163
  } catch (e) {
166
164
  return false;
@@ -168,14 +166,14 @@ async function checkDirectoryExists(directoryPath) {
168
166
  }
169
167
  async function getTmpStoragePath() {
170
168
  const storagePath = path__namespace.default.join(os__default.default.tmpdir(), APP_FOLDER_NAME);
171
- await fse__default.default.ensureDir(storagePath);
169
+ await fse__namespace.default.ensureDir(storagePath);
172
170
  return storagePath;
173
171
  }
174
172
  async function getConfigPath() {
175
173
  const configDirs = XDGAppPaths__default.default(APP_FOLDER_NAME).configDirs();
176
174
  const configPath = configDirs.find(checkDirectoryExists);
177
175
  if (!configPath) {
178
- await fse__default.default.ensureDir(configDirs[0]);
176
+ await fse__namespace.default.ensureDir(configDirs[0]);
179
177
  return configDirs[0];
180
178
  }
181
179
  return configPath;
@@ -183,9 +181,9 @@ async function getConfigPath() {
183
181
  async function getLocalConfig() {
184
182
  const configPath = await getConfigPath();
185
183
  const configFilePath = path__namespace.default.join(configPath, CONFIG_FILENAME);
186
- await fse__default.default.ensureFile(configFilePath);
184
+ await fse__namespace.default.ensureFile(configFilePath);
187
185
  try {
188
- return await fse__default.default.readJSON(configFilePath, { encoding: "utf8", throws: true });
186
+ return await fse__namespace.default.readJSON(configFilePath, { encoding: "utf8", throws: true });
189
187
  } catch (e) {
190
188
  return {};
191
189
  }
@@ -193,10 +191,10 @@ async function getLocalConfig() {
193
191
  async function saveLocalConfig(data) {
194
192
  const configPath = await getConfigPath();
195
193
  const configFilePath = path__namespace.default.join(configPath, CONFIG_FILENAME);
196
- await fse__default.default.writeJson(configFilePath, data, { encoding: "utf8", spaces: 2, mode: 384 });
194
+ await fse__namespace.default.writeJson(configFilePath, data, { encoding: "utf8", spaces: 2, mode: 384 });
197
195
  }
198
196
  const name = "@strapi/cloud-cli";
199
- const version = "4.25.2";
197
+ const version = "4.25.4";
200
198
  const description = "Commands to interact with the Strapi Cloud";
201
199
  const keywords = [
202
200
  "strapi",
@@ -241,7 +239,7 @@ const scripts = {
241
239
  watch: "pack-up watch"
242
240
  };
243
241
  const dependencies = {
244
- "@strapi/utils": "4.25.2",
242
+ "@strapi/utils": "4.25.4",
245
243
  axios: "1.6.0",
246
244
  chalk: "4.1.2",
247
245
  "cli-progress": "3.12.0",
@@ -266,8 +264,8 @@ const devDependencies = {
266
264
  "@types/cli-progress": "3.11.5",
267
265
  "@types/eventsource": "1.1.15",
268
266
  "@types/lodash": "^4.14.191",
269
- "eslint-config-custom": "4.25.2",
270
- tsconfig: "4.25.2"
267
+ "eslint-config-custom": "4.25.4",
268
+ tsconfig: "4.25.4"
271
269
  };
272
270
  const engines = {
273
271
  node: ">=18.0.0 <=20.x.x",
@@ -320,7 +318,7 @@ async function cloudApiFactory({ logger }, token) {
320
318
  deploy({ filePath, project }, { onUploadProgress }) {
321
319
  return axiosCloudAPI.post(
322
320
  `/deploy/${project.name}`,
323
- { file: fse__default.default.createReadStream(filePath) },
321
+ { file: fse__namespace.default.createReadStream(filePath) },
324
322
  {
325
323
  headers: {
326
324
  "Content-Type": "multipart/form-data"
@@ -363,8 +361,33 @@ async function cloudApiFactory({ logger }, token) {
363
361
  throw error;
364
362
  }
365
363
  },
366
- listProjects() {
367
- return axiosCloudAPI.get("/projects");
364
+ async listProjects() {
365
+ try {
366
+ const response = await axiosCloudAPI.get("/projects");
367
+ if (response.status !== 200) {
368
+ throw new Error("Error fetching cloud projects from the server.");
369
+ }
370
+ return response;
371
+ } catch (error) {
372
+ logger.debug(
373
+ "🥲 Oops! Couldn't retrieve your project's list from the server. Please try again."
374
+ );
375
+ throw error;
376
+ }
377
+ },
378
+ async listLinkProjects() {
379
+ try {
380
+ const response = await axiosCloudAPI.get("/projects/linkable");
381
+ if (response.status !== 200) {
382
+ throw new Error("Error fetching cloud projects from the server.");
383
+ }
384
+ return response;
385
+ } catch (error) {
386
+ logger.debug(
387
+ "🥲 Oops! Couldn't retrieve your project's list from the server. Please try again."
388
+ );
389
+ throw error;
390
+ }
368
391
  },
369
392
  track(event, payload = {}) {
370
393
  return axiosCloudAPI.post("/track", {
@@ -379,18 +402,18 @@ async function save(data, { directoryPath } = {}) {
379
402
  const alreadyInFileData = await retrieve({ directoryPath });
380
403
  const storedData = { ...alreadyInFileData, ...data };
381
404
  const pathToFile = path__namespace.default.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
382
- await fse__default.default.ensureDir(path__namespace.default.dirname(pathToFile));
383
- await fse__default.default.writeJson(pathToFile, storedData, { encoding: "utf8" });
405
+ await fse__namespace.default.ensureDir(path__namespace.default.dirname(pathToFile));
406
+ await fse__namespace.default.writeJson(pathToFile, storedData, { encoding: "utf8" });
384
407
  }
385
408
  async function retrieve({
386
409
  directoryPath
387
410
  } = {}) {
388
411
  const pathToFile = path__namespace.default.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
389
- const pathExists = await fse__default.default.pathExists(pathToFile);
412
+ const pathExists = await fse__namespace.default.pathExists(pathToFile);
390
413
  if (!pathExists) {
391
414
  return {};
392
415
  }
393
- return fse__default.default.readJSON(pathToFile, { encoding: "utf8" });
416
+ return fse__namespace.default.readJSON(pathToFile, { encoding: "utf8" });
394
417
  }
395
418
  const strapiInfoSave = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
396
419
  __proto__: null,
@@ -662,7 +685,7 @@ const loadPkg = async ({ cwd, logger }) => {
662
685
  if (!pkgPath) {
663
686
  throw new Error("Could not find a package.json in the current directory");
664
687
  }
665
- const buffer = await fs__default.default.readFile(pkgPath);
688
+ const buffer = await fse__namespace.readFile(pkgPath);
666
689
  const pkg = JSON.parse(buffer.toString());
667
690
  logger.debug("Loaded package.json:", os__default.default.EOL, pkg);
668
691
  return pkg;
@@ -687,6 +710,13 @@ function applyDefaultName(newDefaultName, questions, defaultValues) {
687
710
  });
688
711
  return { newQuestions, newDefaultValues };
689
712
  }
713
+ const trackEvent = async (ctx, cloudApiService, eventName, eventData) => {
714
+ try {
715
+ await cloudApiService.track(eventName, eventData);
716
+ } catch (e) {
717
+ ctx.logger.debug(`Failed to track ${eventName}`, e);
718
+ }
719
+ };
690
720
  const openModule$1 = import("open");
691
721
  async function promptLogin(ctx) {
692
722
  const response = await inquirer__default.default.prompt([
@@ -707,13 +737,6 @@ async function loginAction(ctx) {
707
737
  const tokenService = await tokenServiceFactory(ctx);
708
738
  const existingToken = await tokenService.retrieveToken();
709
739
  const cloudApiService = await cloudApiFactory(ctx, existingToken || void 0);
710
- const trackFailedLogin = async () => {
711
- try {
712
- await cloudApiService.track("didNotLogin", { loginMethod: "cli" });
713
- } catch (e) {
714
- logger.debug("Failed to track failed login", e);
715
- }
716
- };
717
740
  if (existingToken) {
718
741
  const isTokenValid = await tokenService.isTokenValid(existingToken);
719
742
  if (isTokenValid) {
@@ -745,11 +768,7 @@ async function loginAction(ctx) {
745
768
  logger.debug(e);
746
769
  return false;
747
770
  }
748
- try {
749
- await cloudApiService.track("willLoginAttempt", {});
750
- } catch (e) {
751
- logger.debug("Failed to track login attempt", e);
752
- }
771
+ await trackEvent(ctx, cloudApiService, "willLoginAttempt", {});
753
772
  logger.debug("🔐 Creating device authentication request...", {
754
773
  client_id: cliConfig2.clientId,
755
774
  scope: cliConfig2.scope,
@@ -829,13 +848,13 @@ async function loginAction(ctx) {
829
848
  "There seems to be a problem with your login information. Please try logging in again."
830
849
  );
831
850
  spinnerFail();
832
- await trackFailedLogin();
851
+ await trackEvent(ctx, cloudApiService, "didNotLogin", { loginMethod: "cli" });
833
852
  return false;
834
853
  }
835
854
  if (e.response?.data.error && !["authorization_pending", "slow_down"].includes(e.response.data.error)) {
836
855
  logger.debug(e);
837
856
  spinnerFail();
838
- await trackFailedLogin();
857
+ await trackEvent(ctx, cloudApiService, "didNotLogin", { loginMethod: "cli" });
839
858
  return false;
840
859
  }
841
860
  await new Promise((resolve) => {
@@ -849,11 +868,7 @@ async function loginAction(ctx) {
849
868
  "To access your dashboard, please copy and paste the following URL into your web browser:"
850
869
  );
851
870
  logger.log(chalk__default.default.underline(`${apiConfig.dashboardBaseUrl}/projects`));
852
- try {
853
- await cloudApiService.track("didLogin", { loginMethod: "cli" });
854
- } catch (e) {
855
- logger.debug("Failed to track login", e);
856
- }
871
+ await trackEvent(ctx, cloudApiService, "didLogin", { loginMethod: "cli" });
857
872
  };
858
873
  await authenticate();
859
874
  return isAuthenticated;
@@ -902,7 +917,7 @@ async function createProject$1(ctx, cloudApi, projectInput) {
902
917
  throw e;
903
918
  }
904
919
  }
905
- const action$2 = async (ctx) => {
920
+ const action$4 = async (ctx) => {
906
921
  const { logger } = ctx;
907
922
  const { getValidToken, eraseToken } = await tokenServiceFactory(ctx);
908
923
  const token = await getValidToken(ctx, promptLogin);
@@ -1017,6 +1032,7 @@ const buildLogsServiceFactory = ({ logger }) => {
1017
1032
  if (retries > MAX_RETRIES) {
1018
1033
  spinner.fail("We were unable to connect to the server to get build logs at this time.");
1019
1034
  es.close();
1035
+ clearExistingTimeout();
1020
1036
  reject(new Error("Max retries reached"));
1021
1037
  }
1022
1038
  };
@@ -1059,13 +1075,13 @@ async function upload(ctx, project, token, maxProjectFileSize) {
1059
1075
  process.exit(1);
1060
1076
  }
1061
1077
  const tarFilePath = path__namespace.default.resolve(storagePath, compressedFilename);
1062
- const fileStats = await fse__default.default.stat(tarFilePath);
1078
+ const fileStats = await fse__namespace.default.stat(tarFilePath);
1063
1079
  if (fileStats.size > maxProjectFileSize) {
1064
1080
  ctx.logger.log(
1065
1081
  "Unable to proceed: Your project is too big to be transferred, please use a git repo instead."
1066
1082
  );
1067
1083
  try {
1068
- await fse__default.default.remove(tarFilePath);
1084
+ await fse__namespace.default.remove(tarFilePath);
1069
1085
  } catch (e) {
1070
1086
  ctx.logger.log("Unable to remove file: ", tarFilePath);
1071
1087
  ctx.logger.debug(e);
@@ -1104,7 +1120,7 @@ async function upload(ctx, project, token, maxProjectFileSize) {
1104
1120
  }
1105
1121
  ctx.logger.debug(e);
1106
1122
  } finally {
1107
- await fse__default.default.remove(tarFilePath);
1123
+ await fse__namespace.default.remove(tarFilePath);
1108
1124
  }
1109
1125
  process.exit(0);
1110
1126
  } catch (e) {
@@ -1117,7 +1133,7 @@ async function getProject(ctx) {
1117
1133
  const { project } = await retrieve();
1118
1134
  if (!project) {
1119
1135
  try {
1120
- return await action$2(ctx);
1136
+ return await action$4(ctx);
1121
1137
  } catch (e) {
1122
1138
  ctx.logger.error("An error occurred while deploying the project. Please try again later.");
1123
1139
  ctx.logger.debug(e);
@@ -1126,7 +1142,19 @@ async function getProject(ctx) {
1126
1142
  }
1127
1143
  return project;
1128
1144
  }
1129
- const action$1 = async (ctx) => {
1145
+ async function getConfig({
1146
+ ctx,
1147
+ cloudApiService
1148
+ }) {
1149
+ try {
1150
+ const { data: cliConfig2 } = await cloudApiService.config();
1151
+ return cliConfig2;
1152
+ } catch (e) {
1153
+ ctx.logger.debug("Failed to get cli config", e);
1154
+ return null;
1155
+ }
1156
+ }
1157
+ const action$3 = async (ctx) => {
1130
1158
  const { getValidToken } = await tokenServiceFactory(ctx);
1131
1159
  const token = await getValidToken(ctx, promptLogin);
1132
1160
  if (!token) {
@@ -1137,14 +1165,18 @@ const action$1 = async (ctx) => {
1137
1165
  return;
1138
1166
  }
1139
1167
  const cloudApiService = await cloudApiFactory(ctx);
1140
- try {
1141
- await cloudApiService.track("willDeployWithCLI", { projectInternalName: project.name });
1142
- } catch (e) {
1143
- ctx.logger.debug("Failed to track willDeploy", e);
1144
- }
1168
+ await trackEvent(ctx, cloudApiService, "willDeployWithCLI", {
1169
+ projectInternalName: project.name
1170
+ });
1145
1171
  const notificationService = notificationServiceFactory(ctx);
1146
1172
  const buildLogsService = buildLogsServiceFactory(ctx);
1147
- const { data: cliConfig2 } = await cloudApiService.config();
1173
+ const cliConfig2 = await getConfig({ ctx, cloudApiService });
1174
+ if (!cliConfig2) {
1175
+ ctx.logger.error(
1176
+ "An error occurred while retrieving data from Strapi Cloud. Please try check your network or again later."
1177
+ );
1178
+ return;
1179
+ }
1148
1180
  let maxSize = parseInt(cliConfig2.maxProjectFileSize, 10);
1149
1181
  if (Number.isNaN(maxSize)) {
1150
1182
  ctx.logger.debug(
@@ -1166,10 +1198,11 @@ const action$1 = async (ctx) => {
1166
1198
  chalk__default.default.underline(`${apiConfig.dashboardBaseUrl}/projects/${project.name}/deployments`)
1167
1199
  );
1168
1200
  } catch (e) {
1201
+ ctx.logger.debug(e);
1169
1202
  if (e instanceof Error) {
1170
1203
  ctx.logger.error(e.message);
1171
1204
  } else {
1172
- throw e;
1205
+ ctx.logger.error("An error occurred while deploying the project. Please try again later.");
1173
1206
  }
1174
1207
  }
1175
1208
  };
@@ -1200,16 +1233,162 @@ const runAction = (name2, action2) => (...args) => {
1200
1233
  process.exit(1);
1201
1234
  });
1202
1235
  };
1203
- const command$3 = ({ command: command2, ctx }) => {
1204
- command2.command("cloud:deploy").alias("deploy").description("Deploy a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("deploy", action$1)(ctx));
1236
+ const command$5 = ({ command: command2, ctx }) => {
1237
+ command2.command("cloud:deploy").alias("deploy").description("Deploy a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("deploy", action$3)(ctx));
1205
1238
  };
1206
1239
  const deployProject = {
1207
1240
  name: "deploy-project",
1208
1241
  description: "Deploy a Strapi Cloud project",
1209
- action: action$1,
1210
- command: command$3
1242
+ action: action$3,
1243
+ command: command$5
1211
1244
  };
1212
- const command$2 = ({ command: command2, ctx }) => {
1245
+ const QUIT_OPTION = "Quit";
1246
+ async function getExistingConfig(ctx) {
1247
+ try {
1248
+ return await retrieve();
1249
+ } catch (e) {
1250
+ ctx.logger.debug("Failed to get project config", e);
1251
+ ctx.logger.error("An error occurred while retrieving config data from your local project.");
1252
+ return null;
1253
+ }
1254
+ }
1255
+ async function promptForRelink(ctx, cloudApiService, existingConfig) {
1256
+ if (existingConfig && existingConfig.project) {
1257
+ const { shouldRelink } = await inquirer__default.default.prompt([
1258
+ {
1259
+ type: "confirm",
1260
+ name: "shouldRelink",
1261
+ message: `A project named ${chalk__default.default.cyan(
1262
+ existingConfig.project.displayName ? existingConfig.project.displayName : existingConfig.project.name
1263
+ )} is already linked to this local folder. Do you want to update the link?`,
1264
+ default: false
1265
+ }
1266
+ ]);
1267
+ if (!shouldRelink) {
1268
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1269
+ currentProjectName: existingConfig.project?.name
1270
+ });
1271
+ return false;
1272
+ }
1273
+ }
1274
+ return true;
1275
+ }
1276
+ async function getProjectsList(ctx, cloudApiService, existingConfig) {
1277
+ const spinner = ctx.logger.spinner("Fetching your projects...\n").start();
1278
+ try {
1279
+ const {
1280
+ data: { data: projectList }
1281
+ } = await cloudApiService.listLinkProjects();
1282
+ spinner.succeed();
1283
+ if (!Array.isArray(projectList)) {
1284
+ ctx.logger.log("We couldn't find any projects available for linking in Strapi Cloud");
1285
+ return null;
1286
+ }
1287
+ const projects = projectList.filter(
1288
+ (project) => !(project.isMaintainer || project.name === existingConfig?.project?.name)
1289
+ ).map((project) => {
1290
+ return {
1291
+ name: project.displayName,
1292
+ value: { name: project.name, displayName: project.displayName }
1293
+ };
1294
+ });
1295
+ if (projects.length === 0) {
1296
+ ctx.logger.log("We couldn't find any projects available for linking in Strapi Cloud");
1297
+ return null;
1298
+ }
1299
+ return projects;
1300
+ } catch (e) {
1301
+ spinner.fail("An error occurred while fetching your projects from Strapi Cloud.");
1302
+ ctx.logger.debug("Failed to list projects", e);
1303
+ return null;
1304
+ }
1305
+ }
1306
+ async function getUserSelection(ctx, projects) {
1307
+ const { logger } = ctx;
1308
+ try {
1309
+ const answer = await inquirer__default.default.prompt([
1310
+ {
1311
+ type: "list",
1312
+ name: "linkProject",
1313
+ message: "Which project do you want to link?",
1314
+ choices: [...projects, { name: chalk__default.default.grey(`(${QUIT_OPTION})`), value: null }]
1315
+ }
1316
+ ]);
1317
+ if (!answer.linkProject) {
1318
+ return null;
1319
+ }
1320
+ return answer;
1321
+ } catch (e) {
1322
+ logger.debug("Failed to get user input", e);
1323
+ logger.error("An error occurred while trying to get your input.");
1324
+ return null;
1325
+ }
1326
+ }
1327
+ const action$2 = async (ctx) => {
1328
+ const { getValidToken } = await tokenServiceFactory(ctx);
1329
+ const token = await getValidToken(ctx, promptLogin);
1330
+ const { logger } = ctx;
1331
+ if (!token) {
1332
+ return;
1333
+ }
1334
+ const cloudApiService = await cloudApiFactory(ctx, token);
1335
+ const existingConfig = await getExistingConfig(ctx);
1336
+ const shouldRelink = await promptForRelink(ctx, cloudApiService, existingConfig);
1337
+ if (!shouldRelink) {
1338
+ return;
1339
+ }
1340
+ await trackEvent(ctx, cloudApiService, "willLinkProject", {});
1341
+ const projects = await getProjectsList(
1342
+ ctx,
1343
+ cloudApiService,
1344
+ existingConfig
1345
+ );
1346
+ if (!projects) {
1347
+ return;
1348
+ }
1349
+ const answer = await getUserSelection(ctx, projects);
1350
+ if (!answer) {
1351
+ return;
1352
+ }
1353
+ try {
1354
+ const { confirmAction } = await inquirer__default.default.prompt([
1355
+ {
1356
+ type: "confirm",
1357
+ name: "confirmAction",
1358
+ message: "Warning: Once linked, deploying from CLI will replace the existing project and its data. Confirm to proceed:",
1359
+ default: false
1360
+ }
1361
+ ]);
1362
+ if (!confirmAction) {
1363
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1364
+ cancelledProjectName: answer.linkProject.name,
1365
+ currentProjectName: existingConfig ? existingConfig.project?.name : null
1366
+ });
1367
+ return;
1368
+ }
1369
+ await save({ project: answer.linkProject });
1370
+ logger.log(`Project ${chalk__default.default.cyan(answer.linkProject.displayName)} linked successfully.`);
1371
+ await trackEvent(ctx, cloudApiService, "didLinkProject", {
1372
+ projectInternalName: answer.linkProject
1373
+ });
1374
+ } catch (e) {
1375
+ logger.debug("Failed to link project", e);
1376
+ logger.error("An error occurred while linking the project.");
1377
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1378
+ projectInternalName: answer.linkProject
1379
+ });
1380
+ }
1381
+ };
1382
+ const command$4 = ({ command: command2, ctx }) => {
1383
+ command2.command("cloud:link").alias("link").description("Link a local directory to a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("link", action$2)(ctx));
1384
+ };
1385
+ const link = {
1386
+ name: "link-project",
1387
+ description: "Link a local directory to a Strapi Cloud project",
1388
+ action: action$2,
1389
+ command: command$4
1390
+ };
1391
+ const command$3 = ({ command: command2, ctx }) => {
1213
1392
  command2.command("cloud:login").alias("login").description("Strapi Cloud Login").addHelpText(
1214
1393
  "after",
1215
1394
  "\nAfter running this command, you will be prompted to enter your authentication information."
@@ -1219,10 +1398,10 @@ const login = {
1219
1398
  name: "login",
1220
1399
  description: "Strapi Cloud Login",
1221
1400
  action: loginAction,
1222
- command: command$2
1401
+ command: command$3
1223
1402
  };
1224
1403
  const openModule = import("open");
1225
- const action = async (ctx) => {
1404
+ const action$1 = async (ctx) => {
1226
1405
  const { logger } = ctx;
1227
1406
  const { retrieveToken, eraseToken } = await tokenServiceFactory(ctx);
1228
1407
  const token = await retrieveToken();
@@ -1252,37 +1431,64 @@ const action = async (ctx) => {
1252
1431
  logger.error("🥲 Oops! Something went wrong while logging you out. Please try again.");
1253
1432
  logger.debug(e);
1254
1433
  }
1255
- try {
1256
- await cloudApiService.track("didLogout", { loginMethod: "cli" });
1257
- } catch (e) {
1258
- logger.debug("Failed to track logout event", e);
1259
- }
1434
+ await trackEvent(ctx, cloudApiService, "didLogout", { loginMethod: "cli" });
1260
1435
  };
1261
- const command$1 = ({ command: command2, ctx }) => {
1262
- command2.command("cloud:logout").alias("logout").description("Strapi Cloud Logout").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("logout", action)(ctx));
1436
+ const command$2 = ({ command: command2, ctx }) => {
1437
+ command2.command("cloud:logout").alias("logout").description("Strapi Cloud Logout").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("logout", action$1)(ctx));
1263
1438
  };
1264
1439
  const logout = {
1265
1440
  name: "logout",
1266
1441
  description: "Strapi Cloud Logout",
1267
- action,
1268
- command: command$1
1442
+ action: action$1,
1443
+ command: command$2
1269
1444
  };
1270
- const command = ({ command: command2, ctx }) => {
1271
- command2.command("cloud:create-project").description("Create a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("cloud:create-project", action$2)(ctx));
1445
+ const command$1 = ({ command: command2, ctx }) => {
1446
+ command2.command("cloud:create-project").description("Create a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("cloud:create-project", action$4)(ctx));
1272
1447
  };
1273
1448
  const createProject = {
1274
1449
  name: "create-project",
1275
1450
  description: "Create a new project",
1276
- action: action$2,
1451
+ action: action$4,
1452
+ command: command$1
1453
+ };
1454
+ const action = async (ctx) => {
1455
+ const { getValidToken } = await tokenServiceFactory(ctx);
1456
+ const token = await getValidToken(ctx, promptLogin);
1457
+ const { logger } = ctx;
1458
+ if (!token) {
1459
+ return;
1460
+ }
1461
+ const cloudApiService = await cloudApiFactory(ctx, token);
1462
+ const spinner = logger.spinner("Fetching your projects...").start();
1463
+ try {
1464
+ const {
1465
+ data: { data: projectList }
1466
+ } = await cloudApiService.listProjects();
1467
+ spinner.succeed();
1468
+ logger.log(projectList);
1469
+ } catch (e) {
1470
+ ctx.logger.debug("Failed to list projects", e);
1471
+ spinner.fail("An error occurred while fetching your projects from Strapi Cloud.");
1472
+ }
1473
+ };
1474
+ const command = ({ command: command2, ctx }) => {
1475
+ command2.command("cloud:projects").alias("projects").description("List Strapi Cloud projects").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("projects", action)(ctx));
1476
+ };
1477
+ const listProjects = {
1478
+ name: "list-projects",
1479
+ description: "List Strapi Cloud projects",
1480
+ action,
1277
1481
  command
1278
1482
  };
1279
1483
  const cli = {
1280
1484
  deployProject,
1485
+ link,
1281
1486
  login,
1282
1487
  logout,
1283
- createProject
1488
+ createProject,
1489
+ listProjects
1284
1490
  };
1285
- const cloudCommands = [deployProject, login, logout];
1491
+ const cloudCommands = [deployProject, link, login, logout, listProjects];
1286
1492
  async function initCloudCLIConfig() {
1287
1493
  const localConfig = await getLocalConfig();
1288
1494
  if (!localConfig.deviceId) {