@strapi/cloud-cli 0.0.0-next.a64ced5364618f917adb31b8684b4e3c01c58862 → 0.0.0-next.ac654f8b8646bf964ebd39d4313c4afab0917a24

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.
Files changed (68) hide show
  1. package/LICENSE +18 -3
  2. package/dist/bin.js +1 -0
  3. package/dist/bin.js.map +1 -1
  4. package/dist/index.js +1019 -324
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1021 -324
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/src/cloud/command.d.ts +3 -0
  9. package/dist/src/cloud/command.d.ts.map +1 -0
  10. package/dist/src/create-project/action.d.ts +1 -1
  11. package/dist/src/create-project/action.d.ts.map +1 -1
  12. package/dist/src/create-project/command.d.ts.map +1 -1
  13. package/dist/src/create-project/utils/get-project-name-from-pkg.d.ts +3 -0
  14. package/dist/src/create-project/utils/get-project-name-from-pkg.d.ts.map +1 -0
  15. package/dist/src/create-project/utils/project-questions.utils.d.ts +20 -0
  16. package/dist/src/create-project/utils/project-questions.utils.d.ts.map +1 -0
  17. package/dist/src/deploy-project/action.d.ts +5 -1
  18. package/dist/src/deploy-project/action.d.ts.map +1 -1
  19. package/dist/src/deploy-project/command.d.ts.map +1 -1
  20. package/dist/src/environment/command.d.ts +3 -0
  21. package/dist/src/environment/command.d.ts.map +1 -0
  22. package/dist/src/environment/link/action.d.ts +4 -0
  23. package/dist/src/environment/link/action.d.ts.map +1 -0
  24. package/dist/src/environment/link/command.d.ts +4 -0
  25. package/dist/src/environment/link/command.d.ts.map +1 -0
  26. package/dist/src/environment/link/index.d.ts +7 -0
  27. package/dist/src/environment/link/index.d.ts.map +1 -0
  28. package/dist/src/environment/list/action.d.ts +4 -0
  29. package/dist/src/environment/list/action.d.ts.map +1 -0
  30. package/dist/src/environment/list/command.d.ts +4 -0
  31. package/dist/src/environment/list/command.d.ts.map +1 -0
  32. package/dist/src/environment/list/index.d.ts +7 -0
  33. package/dist/src/environment/list/index.d.ts.map +1 -0
  34. package/dist/src/index.d.ts +4 -0
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/link/action.d.ts +4 -0
  37. package/dist/src/link/action.d.ts.map +1 -0
  38. package/dist/src/link/command.d.ts +7 -0
  39. package/dist/src/link/command.d.ts.map +1 -0
  40. package/dist/src/link/index.d.ts +7 -0
  41. package/dist/src/link/index.d.ts.map +1 -0
  42. package/dist/src/list-projects/action.d.ts +4 -0
  43. package/dist/src/list-projects/action.d.ts.map +1 -0
  44. package/dist/src/list-projects/command.d.ts +7 -0
  45. package/dist/src/list-projects/command.d.ts.map +1 -0
  46. package/dist/src/list-projects/index.d.ts +7 -0
  47. package/dist/src/list-projects/index.d.ts.map +1 -0
  48. package/dist/src/login/action.d.ts +2 -2
  49. package/dist/src/login/action.d.ts.map +1 -1
  50. package/dist/src/login/command.d.ts.map +1 -1
  51. package/dist/src/logout/action.d.ts.map +1 -1
  52. package/dist/src/logout/command.d.ts.map +1 -1
  53. package/dist/src/services/build-logs.d.ts.map +1 -1
  54. package/dist/src/services/cli-api.d.ts +67 -10
  55. package/dist/src/services/cli-api.d.ts.map +1 -1
  56. package/dist/src/services/strapi-info-save.d.ts +15 -2
  57. package/dist/src/services/strapi-info-save.d.ts.map +1 -1
  58. package/dist/src/services/token.d.ts +1 -1
  59. package/dist/src/services/token.d.ts.map +1 -1
  60. package/dist/src/types.d.ts +8 -1
  61. package/dist/src/types.d.ts.map +1 -1
  62. package/dist/src/utils/analytics.d.ts +4 -0
  63. package/dist/src/utils/analytics.d.ts.map +1 -0
  64. package/dist/src/utils/compress-files.d.ts.map +1 -1
  65. package/dist/src/utils/get-local-config.d.ts +6 -0
  66. package/dist/src/utils/get-local-config.d.ts.map +1 -0
  67. package/dist/src/utils/pkg.d.ts.map +1 -1
  68. package/package.json +11 -9
package/dist/index.mjs CHANGED
@@ -1,27 +1,29 @@
1
1
  import crypto$1 from "crypto";
2
- import fse from "fs-extra";
2
+ import * as fse from "fs-extra";
3
+ import fse__default from "fs-extra";
4
+ import inquirer from "inquirer";
5
+ import boxen from "boxen";
3
6
  import * as path from "path";
4
7
  import path__default from "path";
5
8
  import chalk from "chalk";
6
9
  import axios, { AxiosError } from "axios";
7
10
  import * as crypto from "node:crypto";
8
11
  import { env } from "@strapi/utils";
9
- import * as fs from "fs";
10
12
  import * as tar from "tar";
11
13
  import { minimatch } from "minimatch";
12
- import inquirer from "inquirer";
13
14
  import { defaults, has } from "lodash/fp";
14
15
  import os from "os";
15
16
  import XDGAppPaths from "xdg-app-paths";
17
+ import { merge } from "lodash";
16
18
  import jwksClient from "jwks-rsa";
17
19
  import jwt from "jsonwebtoken";
18
20
  import stringify from "fast-safe-stringify";
19
21
  import ora from "ora";
20
22
  import * as cliProgress from "cli-progress";
21
- import EventSource from "eventsource";
22
- import fs$1 from "fs/promises";
23
23
  import pkgUp from "pkg-up";
24
24
  import * as yup from "yup";
25
+ import EventSource from "eventsource";
26
+ import { createCommand } from "commander";
25
27
  const apiConfig = {
26
28
  apiBaseUrl: env("STRAPI_CLI_CLOUD_API", "https://cloud-cli-api.strapi.io"),
27
29
  dashboardBaseUrl: env("STRAPI_CLI_CLOUD_DASHBOARD", "https://cloud.strapi.io")
@@ -40,23 +42,6 @@ const IGNORED_PATTERNS = [
40
42
  "**/.idea/**",
41
43
  "**/.vscode/**"
42
44
  ];
43
- const getFiles = (dirPath, ignorePatterns = [], arrayOfFiles = [], subfolder = "") => {
44
- const entries = fs.readdirSync(path.join(dirPath, subfolder));
45
- entries.forEach((entry) => {
46
- const entryPathFromRoot = path.join(subfolder, entry);
47
- const entryPath = path.relative(dirPath, entryPathFromRoot);
48
- const isIgnored = isIgnoredFile(dirPath, entryPathFromRoot, ignorePatterns);
49
- if (isIgnored) {
50
- return;
51
- }
52
- if (fs.statSync(entryPath).isDirectory()) {
53
- getFiles(dirPath, ignorePatterns, arrayOfFiles, entryPathFromRoot);
54
- } else {
55
- arrayOfFiles.push(entryPath);
56
- }
57
- });
58
- return arrayOfFiles;
59
- };
60
45
  const isIgnoredFile = (folderPath, file, ignorePatterns) => {
61
46
  ignorePatterns.push(...IGNORED_PATTERNS);
62
47
  const relativeFilePath = path.join(folderPath, file);
@@ -74,16 +59,35 @@ const isIgnoredFile = (folderPath, file, ignorePatterns) => {
74
59
  }
75
60
  return isIgnored;
76
61
  };
77
- const readGitignore = (folderPath) => {
62
+ const getFiles = async (dirPath, ignorePatterns = [], subfolder = "") => {
63
+ const arrayOfFiles = [];
64
+ const entries = await fse.readdir(path.join(dirPath, subfolder));
65
+ for (const entry of entries) {
66
+ const entryPathFromRoot = path.join(subfolder, entry);
67
+ const entryPath = path.relative(dirPath, entryPathFromRoot);
68
+ const isIgnored = isIgnoredFile(dirPath, entryPathFromRoot, ignorePatterns);
69
+ if (!isIgnored) {
70
+ if (fse.statSync(entryPath).isDirectory()) {
71
+ const subFiles = await getFiles(dirPath, ignorePatterns, entryPathFromRoot);
72
+ arrayOfFiles.push(...subFiles);
73
+ } else {
74
+ arrayOfFiles.push(entryPath);
75
+ }
76
+ }
77
+ }
78
+ return arrayOfFiles;
79
+ };
80
+ const readGitignore = async (folderPath) => {
78
81
  const gitignorePath = path.resolve(folderPath, ".gitignore");
79
- if (!fs.existsSync(gitignorePath))
82
+ const pathExist = await fse.pathExists(gitignorePath);
83
+ if (!pathExist)
80
84
  return [];
81
- const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
85
+ const gitignoreContent = await fse.readFile(gitignorePath, "utf8");
82
86
  return gitignoreContent.split(/\r?\n/).filter((line) => Boolean(line.trim()) && !line.startsWith("#"));
83
87
  };
84
88
  const compressFilesToTar = async (storagePath, folderToCompress, filename) => {
85
- const ignorePatterns = readGitignore(folderToCompress);
86
- const filesToCompress = getFiles(folderToCompress, ignorePatterns);
89
+ const ignorePatterns = await readGitignore(folderToCompress);
90
+ const filesToCompress = await getFiles(folderToCompress, ignorePatterns);
87
91
  return tar.c(
88
92
  {
89
93
  gzip: true,
@@ -96,7 +100,7 @@ const APP_FOLDER_NAME = "com.strapi.cli";
96
100
  const CONFIG_FILENAME = "config.json";
97
101
  async function checkDirectoryExists(directoryPath) {
98
102
  try {
99
- const fsStat = await fse.lstat(directoryPath);
103
+ const fsStat = await fse__default.lstat(directoryPath);
100
104
  return fsStat.isDirectory();
101
105
  } catch (e) {
102
106
  return false;
@@ -104,24 +108,24 @@ async function checkDirectoryExists(directoryPath) {
104
108
  }
105
109
  async function getTmpStoragePath() {
106
110
  const storagePath = path__default.join(os.tmpdir(), APP_FOLDER_NAME);
107
- await fse.ensureDir(storagePath);
111
+ await fse__default.ensureDir(storagePath);
108
112
  return storagePath;
109
113
  }
110
114
  async function getConfigPath() {
111
115
  const configDirs = XDGAppPaths(APP_FOLDER_NAME).configDirs();
112
116
  const configPath = configDirs.find(checkDirectoryExists);
113
117
  if (!configPath) {
114
- await fse.ensureDir(configDirs[0]);
118
+ await fse__default.ensureDir(configDirs[0]);
115
119
  return configDirs[0];
116
120
  }
117
121
  return configPath;
118
122
  }
119
- async function getLocalConfig() {
123
+ async function getLocalConfig$1() {
120
124
  const configPath = await getConfigPath();
121
125
  const configFilePath = path__default.join(configPath, CONFIG_FILENAME);
122
- await fse.ensureFile(configFilePath);
126
+ await fse__default.ensureFile(configFilePath);
123
127
  try {
124
- return await fse.readJSON(configFilePath, { encoding: "utf8", throws: true });
128
+ return await fse__default.readJSON(configFilePath, { encoding: "utf8", throws: true });
125
129
  } catch (e) {
126
130
  return {};
127
131
  }
@@ -129,10 +133,10 @@ async function getLocalConfig() {
129
133
  async function saveLocalConfig(data) {
130
134
  const configPath = await getConfigPath();
131
135
  const configFilePath = path__default.join(configPath, CONFIG_FILENAME);
132
- await fse.writeJson(configFilePath, data, { encoding: "utf8", spaces: 2, mode: 384 });
136
+ await fse__default.writeJson(configFilePath, data, { encoding: "utf8", spaces: 2, mode: 384 });
133
137
  }
134
138
  const name = "@strapi/cloud-cli";
135
- const version = "4.25.0";
139
+ const version = "5.4.0";
136
140
  const description = "Commands to interact with the Strapi Cloud";
137
141
  const keywords = [
138
142
  "strapi",
@@ -173,17 +177,19 @@ const scripts = {
173
177
  build: "pack-up build",
174
178
  clean: "run -T rimraf ./dist",
175
179
  lint: "run -T eslint .",
180
+ "test:unit": "run -T jest",
176
181
  watch: "pack-up watch"
177
182
  };
178
183
  const dependencies = {
179
- "@strapi/utils": "4.25.0",
180
- axios: "1.6.0",
184
+ "@strapi/utils": "5.4.0",
185
+ axios: "1.7.4",
186
+ boxen: "5.1.2",
181
187
  chalk: "4.1.2",
182
188
  "cli-progress": "3.12.0",
183
189
  commander: "8.3.0",
184
190
  eventsource: "2.0.2",
185
191
  "fast-safe-stringify": "2.1.1",
186
- "fs-extra": "10.0.0",
192
+ "fs-extra": "11.2.0",
187
193
  inquirer: "8.2.5",
188
194
  jsonwebtoken: "9.0.0",
189
195
  "jwks-rsa": "3.1.0",
@@ -192,7 +198,7 @@ const dependencies = {
192
198
  open: "8.4.0",
193
199
  ora: "5.4.1",
194
200
  "pkg-up": "3.1.0",
195
- tar: "6.1.13",
201
+ tar: "6.2.1",
196
202
  "xdg-app-paths": "8.3.0",
197
203
  yup: "0.32.9"
198
204
  };
@@ -201,13 +207,14 @@ const devDependencies = {
201
207
  "@types/cli-progress": "3.11.5",
202
208
  "@types/eventsource": "1.1.15",
203
209
  "@types/lodash": "^4.14.191",
204
- "eslint-config-custom": "4.25.0",
205
- tsconfig: "4.25.0"
210
+ "eslint-config-custom": "5.4.0",
211
+ tsconfig: "5.4.0"
206
212
  };
207
213
  const engines = {
208
- node: ">=18.0.0 <=20.x.x",
214
+ node: ">=18.0.0 <=22.x.x",
209
215
  npm: ">=6.0.0"
210
216
  };
217
+ const gitHead = "7d785703f52464577d077c4618cbe68b44f8a9cd";
211
218
  const packageJson = {
212
219
  name,
213
220
  version,
@@ -228,11 +235,12 @@ const packageJson = {
228
235
  scripts,
229
236
  dependencies,
230
237
  devDependencies,
231
- engines
238
+ engines,
239
+ gitHead
232
240
  };
233
241
  const VERSION = "v1";
234
- async function cloudApiFactory(token) {
235
- const localConfig = await getLocalConfig();
242
+ async function cloudApiFactory({ logger }, token) {
243
+ const localConfig = await getLocalConfig$1();
236
244
  const customHeaders = {
237
245
  "x-device-id": localConfig.deviceId,
238
246
  "x-app-version": packageJson.version,
@@ -255,7 +263,7 @@ async function cloudApiFactory(token) {
255
263
  deploy({ filePath, project }, { onUploadProgress }) {
256
264
  return axiosCloudAPI.post(
257
265
  `/deploy/${project.name}`,
258
- { file: fse.createReadStream(filePath) },
266
+ { file: fse__default.createReadStream(filePath), targetEnvironment: project.targetEnvironment },
259
267
  {
260
268
  headers: {
261
269
  "Content-Type": "multipart/form-data"
@@ -284,11 +292,89 @@ async function cloudApiFactory(token) {
284
292
  getUserInfo() {
285
293
  return axiosCloudAPI.get("/user");
286
294
  },
287
- config() {
288
- return axiosCloudAPI.get("/config");
295
+ async config() {
296
+ try {
297
+ const response = await axiosCloudAPI.get("/config");
298
+ if (response.status !== 200) {
299
+ throw new Error("Error fetching cloud CLI config from the server.");
300
+ }
301
+ return response;
302
+ } catch (error) {
303
+ logger.debug(
304
+ "🥲 Oops! Couldn't retrieve the cloud CLI config from the server. Please try again."
305
+ );
306
+ throw error;
307
+ }
308
+ },
309
+ async listProjects() {
310
+ try {
311
+ const response = await axiosCloudAPI.get("/projects");
312
+ if (response.status !== 200) {
313
+ throw new Error("Error fetching cloud projects from the server.");
314
+ }
315
+ return response;
316
+ } catch (error) {
317
+ logger.debug(
318
+ "🥲 Oops! Couldn't retrieve your project's list from the server. Please try again."
319
+ );
320
+ throw error;
321
+ }
322
+ },
323
+ async listLinkProjects() {
324
+ try {
325
+ const response = await axiosCloudAPI.get("/projects-linkable");
326
+ if (response.status !== 200) {
327
+ throw new Error("Error fetching cloud projects from the server.");
328
+ }
329
+ return response;
330
+ } catch (error) {
331
+ logger.debug(
332
+ "🥲 Oops! Couldn't retrieve your project's list from the server. Please try again."
333
+ );
334
+ throw error;
335
+ }
336
+ },
337
+ async listEnvironments({ name: name2 }) {
338
+ try {
339
+ const response = await axiosCloudAPI.get(`/projects/${name2}/environments`);
340
+ if (response.status !== 200) {
341
+ throw new Error("Error fetching cloud environments from the server.");
342
+ }
343
+ return response;
344
+ } catch (error) {
345
+ logger.debug(
346
+ "🥲 Oops! Couldn't retrieve your project's environments from the server. Please try again."
347
+ );
348
+ throw error;
349
+ }
350
+ },
351
+ async listLinkEnvironments({ name: name2 }) {
352
+ try {
353
+ const response = await axiosCloudAPI.get(`/projects/${name2}/environments-linkable`);
354
+ if (response.status !== 200) {
355
+ throw new Error("Error fetching cloud environments from the server.");
356
+ }
357
+ return response;
358
+ } catch (error) {
359
+ logger.debug(
360
+ "🥲 Oops! Couldn't retrieve your project's environments from the server. Please try again."
361
+ );
362
+ throw error;
363
+ }
289
364
  },
290
- listProjects() {
291
- return axiosCloudAPI.get("/projects");
365
+ async getProject({ name: name2 }) {
366
+ try {
367
+ const response = await axiosCloudAPI.get(`/projects/${name2}`);
368
+ if (response.status !== 200) {
369
+ throw new Error("Error fetching project's details.");
370
+ }
371
+ return response;
372
+ } catch (error) {
373
+ logger.debug(
374
+ "🥲 Oops! There was a problem retrieving your project's details. Please try again."
375
+ );
376
+ throw error;
377
+ }
292
378
  },
293
379
  track(event, payload = {}) {
294
380
  return axiosCloudAPI.post("/track", {
@@ -299,34 +385,51 @@ async function cloudApiFactory(token) {
299
385
  };
300
386
  }
301
387
  const LOCAL_SAVE_FILENAME = ".strapi-cloud.json";
388
+ const getFilePath = (directoryPath) => path__default.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
302
389
  async function save(data, { directoryPath } = {}) {
303
- const alreadyInFileData = await retrieve({ directoryPath });
304
- const storedData = { ...alreadyInFileData, ...data };
305
- const pathToFile = path__default.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
306
- await fse.ensureDir(path__default.dirname(pathToFile));
307
- await fse.writeJson(pathToFile, storedData, { encoding: "utf8" });
390
+ const pathToFile = getFilePath(directoryPath);
391
+ await fse__default.ensureDir(path__default.dirname(pathToFile));
392
+ await fse__default.writeJson(pathToFile, data, { encoding: "utf8" });
308
393
  }
309
394
  async function retrieve({
310
395
  directoryPath
311
396
  } = {}) {
312
- const pathToFile = path__default.join(directoryPath || process.cwd(), LOCAL_SAVE_FILENAME);
313
- const pathExists = await fse.pathExists(pathToFile);
397
+ const pathToFile = getFilePath(directoryPath);
398
+ const pathExists = await fse__default.pathExists(pathToFile);
314
399
  if (!pathExists) {
315
400
  return {};
316
401
  }
317
- return fse.readJSON(pathToFile, { encoding: "utf8" });
402
+ return fse__default.readJSON(pathToFile, { encoding: "utf8" });
403
+ }
404
+ async function patch(patchData, { directoryPath } = {}) {
405
+ const pathToFile = getFilePath(directoryPath);
406
+ const existingData = await retrieve({ directoryPath });
407
+ if (!existingData) {
408
+ throw new Error("No configuration data found to patch.");
409
+ }
410
+ const newData = merge(existingData, patchData);
411
+ await fse__default.writeJson(pathToFile, newData, { encoding: "utf8" });
412
+ }
413
+ async function deleteConfig({ directoryPath } = {}) {
414
+ const pathToFile = getFilePath(directoryPath);
415
+ const pathExists = await fse__default.pathExists(pathToFile);
416
+ if (pathExists) {
417
+ await fse__default.remove(pathToFile);
418
+ }
318
419
  }
319
420
  const strapiInfoSave = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
320
421
  __proto__: null,
321
422
  LOCAL_SAVE_FILENAME,
423
+ deleteConfig,
424
+ patch,
322
425
  retrieve,
323
426
  save
324
427
  }, Symbol.toStringTag, { value: "Module" }));
325
428
  let cliConfig;
326
429
  async function tokenServiceFactory({ logger }) {
327
- const cloudApiService = await cloudApiFactory();
430
+ const cloudApiService = await cloudApiFactory({ logger });
328
431
  async function saveToken(str) {
329
- const appConfig = await getLocalConfig();
432
+ const appConfig = await getLocalConfig$1();
330
433
  if (!appConfig) {
331
434
  logger.error("There was a problem saving your token. Please try again.");
332
435
  return;
@@ -340,7 +443,7 @@ async function tokenServiceFactory({ logger }) {
340
443
  }
341
444
  }
342
445
  async function retrieveToken() {
343
- const appConfig = await getLocalConfig();
446
+ const appConfig = await getLocalConfig$1();
344
447
  if (appConfig.token) {
345
448
  if (await isTokenValid(appConfig.token)) {
346
449
  return appConfig.token;
@@ -373,14 +476,17 @@ async function tokenServiceFactory({ logger }) {
373
476
  "There seems to be a problem with your login information. Please try logging in again."
374
477
  );
375
478
  }
479
+ return Promise.reject(new Error("Invalid token"));
376
480
  }
377
481
  return new Promise((resolve, reject) => {
378
482
  jwt.verify(idToken, getKey, (err) => {
379
483
  if (err) {
380
484
  reject(err);
381
- } else {
382
- resolve();
383
485
  }
486
+ if (decodedToken.payload.exp < Math.floor(Date.now() / 1e3)) {
487
+ reject(new Error("Token is expired"));
488
+ }
489
+ resolve();
384
490
  });
385
491
  });
386
492
  }
@@ -399,7 +505,7 @@ async function tokenServiceFactory({ logger }) {
399
505
  }
400
506
  }
401
507
  async function eraseToken() {
402
- const appConfig = await getLocalConfig();
508
+ const appConfig = await getLocalConfig$1();
403
509
  if (!appConfig) {
404
510
  return;
405
511
  }
@@ -414,15 +520,15 @@ async function tokenServiceFactory({ logger }) {
414
520
  throw e;
415
521
  }
416
522
  }
417
- async function getValidToken() {
418
- const token = await retrieveToken();
419
- if (!token) {
420
- logger.log("No token found. Please login first.");
421
- return null;
422
- }
423
- if (!await isTokenValid(token)) {
424
- logger.log("Unable to proceed: Token is expired or not valid. Please login again.");
425
- return null;
523
+ async function getValidToken(ctx, loginAction2) {
524
+ let token = await retrieveToken();
525
+ while (!token || !await isTokenValid(token)) {
526
+ logger.log(
527
+ token ? "Oops! Your token seems expired or invalid. Please login again." : "We couldn't find a valid token. You need to be logged in to use this feature."
528
+ );
529
+ if (!await loginAction2(ctx))
530
+ return null;
531
+ token = await retrieveToken();
426
532
  }
427
533
  return token;
428
534
  }
@@ -556,17 +662,257 @@ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
556
662
  local: strapiInfoSave,
557
663
  tokenServiceFactory
558
664
  }, Symbol.toStringTag, { value: "Module" }));
559
- async function handleError(ctx, error) {
665
+ yup.object({
666
+ name: yup.string().required(),
667
+ exports: yup.lazy(
668
+ (value) => yup.object(
669
+ typeof value === "object" ? Object.entries(value).reduce(
670
+ (acc, [key, value2]) => {
671
+ if (typeof value2 === "object") {
672
+ acc[key] = yup.object({
673
+ types: yup.string().optional(),
674
+ source: yup.string().required(),
675
+ module: yup.string().optional(),
676
+ import: yup.string().required(),
677
+ require: yup.string().required(),
678
+ default: yup.string().required()
679
+ }).noUnknown(true);
680
+ } else {
681
+ acc[key] = yup.string().matches(/^\.\/.*\.json$/).required();
682
+ }
683
+ return acc;
684
+ },
685
+ {}
686
+ ) : void 0
687
+ ).optional()
688
+ )
689
+ });
690
+ const loadPkg = async ({ cwd, logger }) => {
691
+ const pkgPath = await pkgUp({ cwd });
692
+ if (!pkgPath) {
693
+ throw new Error("Could not find a package.json in the current directory");
694
+ }
695
+ const buffer = await fse.readFile(pkgPath);
696
+ const pkg = JSON.parse(buffer.toString());
697
+ logger.debug("Loaded package.json:", os.EOL, pkg);
698
+ return pkg;
699
+ };
700
+ async function getProjectNameFromPackageJson(ctx) {
701
+ try {
702
+ const packageJson2 = await loadPkg(ctx);
703
+ return packageJson2.name || "my-strapi-project";
704
+ } catch (e) {
705
+ return "my-strapi-project";
706
+ }
707
+ }
708
+ const trackEvent = async (ctx, cloudApiService, eventName, eventData) => {
709
+ try {
710
+ await cloudApiService.track(eventName, eventData);
711
+ } catch (e) {
712
+ ctx.logger.debug(`Failed to track ${eventName}`, e);
713
+ }
714
+ };
715
+ const openModule$1 = import("open");
716
+ async function promptLogin(ctx) {
717
+ const response = await inquirer.prompt([
718
+ {
719
+ type: "confirm",
720
+ name: "login",
721
+ message: "Would you like to login?"
722
+ }
723
+ ]);
724
+ if (response.login) {
725
+ const loginSuccessful = await loginAction(ctx);
726
+ return loginSuccessful;
727
+ }
728
+ return false;
729
+ }
730
+ async function loginAction(ctx) {
731
+ const { logger } = ctx;
560
732
  const tokenService = await tokenServiceFactory(ctx);
733
+ const existingToken = await tokenService.retrieveToken();
734
+ const cloudApiService = await cloudApiFactory(ctx, existingToken || void 0);
735
+ if (existingToken) {
736
+ const isTokenValid = await tokenService.isTokenValid(existingToken);
737
+ if (isTokenValid) {
738
+ try {
739
+ const userInfo = await cloudApiService.getUserInfo();
740
+ const { email } = userInfo.data.data;
741
+ if (email) {
742
+ logger.log(`You are already logged into your account (${email}).`);
743
+ } else {
744
+ logger.log("You are already logged in.");
745
+ }
746
+ logger.log(
747
+ "To access your dashboard, please copy and paste the following URL into your web browser:"
748
+ );
749
+ logger.log(chalk.underline(`${apiConfig.dashboardBaseUrl}/projects`));
750
+ return true;
751
+ } catch (e) {
752
+ logger.debug("Failed to fetch user info", e);
753
+ }
754
+ }
755
+ }
756
+ let cliConfig2;
757
+ try {
758
+ logger.info("🔌 Connecting to the Strapi Cloud API...");
759
+ const config = await cloudApiService.config();
760
+ cliConfig2 = config.data;
761
+ } catch (e) {
762
+ logger.error("🥲 Oops! Something went wrong while logging you in. Please try again.");
763
+ logger.debug(e);
764
+ return false;
765
+ }
766
+ await trackEvent(ctx, cloudApiService, "willLoginAttempt", {});
767
+ logger.debug("🔐 Creating device authentication request...", {
768
+ client_id: cliConfig2.clientId,
769
+ scope: cliConfig2.scope,
770
+ audience: cliConfig2.audience
771
+ });
772
+ const deviceAuthResponse = await axios.post(cliConfig2.deviceCodeAuthUrl, {
773
+ client_id: cliConfig2.clientId,
774
+ scope: cliConfig2.scope,
775
+ audience: cliConfig2.audience
776
+ }).catch((e) => {
777
+ logger.error("There was an issue with the authentication process. Please try again.");
778
+ if (e.message) {
779
+ logger.debug(e.message, e);
780
+ } else {
781
+ logger.debug(e);
782
+ }
783
+ });
784
+ openModule$1.then((open) => {
785
+ open.default(deviceAuthResponse.data.verification_uri_complete).catch((e) => {
786
+ logger.error("We encountered an issue opening the browser. Please try again later.");
787
+ logger.debug(e.message, e);
788
+ });
789
+ });
790
+ logger.log("If a browser tab does not open automatically, please follow the next steps:");
791
+ logger.log(
792
+ `1. Open this url in your device: ${deviceAuthResponse.data.verification_uri_complete}`
793
+ );
794
+ logger.log(
795
+ `2. Enter the following code: ${deviceAuthResponse.data.user_code} and confirm to login.
796
+ `
797
+ );
798
+ const tokenPayload = {
799
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
800
+ device_code: deviceAuthResponse.data.device_code,
801
+ client_id: cliConfig2.clientId
802
+ };
803
+ let isAuthenticated = false;
804
+ const authenticate = async () => {
805
+ const spinner = logger.spinner("Waiting for authentication");
806
+ spinner.start();
807
+ const spinnerFail = () => spinner.fail("Authentication failed!");
808
+ while (!isAuthenticated) {
809
+ try {
810
+ const tokenResponse = await axios.post(cliConfig2.tokenUrl, tokenPayload);
811
+ const authTokenData = tokenResponse.data;
812
+ if (tokenResponse.status === 200) {
813
+ try {
814
+ logger.debug("🔐 Validating token...");
815
+ await tokenService.validateToken(authTokenData.id_token, cliConfig2.jwksUrl);
816
+ logger.debug("🔐 Token validation successful!");
817
+ } catch (e) {
818
+ logger.debug(e);
819
+ spinnerFail();
820
+ throw new Error("Unable to proceed: Token validation failed");
821
+ }
822
+ logger.debug("🔍 Fetching user information...");
823
+ const cloudApiServiceWithToken = await cloudApiFactory(ctx, authTokenData.access_token);
824
+ await cloudApiServiceWithToken.getUserInfo();
825
+ logger.debug("🔍 User information fetched successfully!");
826
+ try {
827
+ logger.debug("📝 Saving login information...");
828
+ await tokenService.saveToken(authTokenData.access_token);
829
+ logger.debug("📝 Login information saved successfully!");
830
+ isAuthenticated = true;
831
+ } catch (e) {
832
+ logger.error(
833
+ "There was a problem saving your login information. Please try logging in again."
834
+ );
835
+ logger.debug(e);
836
+ spinnerFail();
837
+ return false;
838
+ }
839
+ }
840
+ } catch (e) {
841
+ if (e.message === "Unable to proceed: Token validation failed") {
842
+ logger.error(
843
+ "There seems to be a problem with your login information. Please try logging in again."
844
+ );
845
+ spinnerFail();
846
+ await trackEvent(ctx, cloudApiService, "didNotLogin", { loginMethod: "cli" });
847
+ return false;
848
+ }
849
+ if (e.response?.data.error && !["authorization_pending", "slow_down"].includes(e.response.data.error)) {
850
+ logger.debug(e);
851
+ spinnerFail();
852
+ await trackEvent(ctx, cloudApiService, "didNotLogin", { loginMethod: "cli" });
853
+ return false;
854
+ }
855
+ await new Promise((resolve) => {
856
+ setTimeout(resolve, deviceAuthResponse.data.interval * 1e3);
857
+ });
858
+ }
859
+ }
860
+ spinner.succeed("Authentication successful!");
861
+ logger.log("You are now logged into Strapi Cloud.");
862
+ logger.log(
863
+ "To access your dashboard, please copy and paste the following URL into your web browser:"
864
+ );
865
+ logger.log(chalk.underline(`${apiConfig.dashboardBaseUrl}/projects`));
866
+ await trackEvent(ctx, cloudApiService, "didLogin", { loginMethod: "cli" });
867
+ };
868
+ await authenticate();
869
+ return isAuthenticated;
870
+ }
871
+ function questionDefaultValuesMapper(questionsMap) {
872
+ return (questions) => {
873
+ return questions.map((question) => {
874
+ const questionName = question.name;
875
+ if (questionName in questionsMap) {
876
+ const questionDefault = questionsMap[questionName];
877
+ if (typeof questionDefault === "function") {
878
+ return {
879
+ ...question,
880
+ default: questionDefault(question)
881
+ };
882
+ }
883
+ return {
884
+ ...question,
885
+ default: questionDefault
886
+ };
887
+ }
888
+ return question;
889
+ });
890
+ };
891
+ }
892
+ function getDefaultsFromQuestions(questions) {
893
+ return questions.reduce((acc, question) => {
894
+ if (question.default && question.name) {
895
+ return { ...acc, [question.name]: question.default };
896
+ }
897
+ return acc;
898
+ }, {});
899
+ }
900
+ function getProjectNodeVersionDefault(question) {
901
+ const currentNodeVersion = process.versions.node.split(".")[0];
902
+ if (question.type === "list" && Array.isArray(question.choices)) {
903
+ const choice = question.choices.find((choice2) => choice2.value === currentNodeVersion);
904
+ if (choice) {
905
+ return choice.value;
906
+ }
907
+ }
908
+ return question.default;
909
+ }
910
+ async function handleError(ctx, error) {
561
911
  const { logger } = ctx;
562
912
  logger.debug(error);
563
913
  if (error instanceof AxiosError) {
564
914
  const errorMessage = typeof error.response?.data === "string" ? error.response.data : null;
565
915
  switch (error.response?.status) {
566
- case 401:
567
- logger.error("Your session has expired. Please log in again.");
568
- await tokenService.eraseToken();
569
- return;
570
916
  case 403:
571
917
  logger.error(
572
918
  errorMessage || "You do not have permission to create a project. Please contact support for assistance."
@@ -592,19 +938,8 @@ async function handleError(ctx, error) {
592
938
  "We encountered an issue while creating your project. Please try again in a moment. If the problem persists, contact support for assistance."
593
939
  );
594
940
  }
595
- const action$3 = async (ctx) => {
941
+ async function createProject$1(ctx, cloudApi, projectInput) {
596
942
  const { logger } = ctx;
597
- const { getValidToken } = await tokenServiceFactory(ctx);
598
- const token = await getValidToken();
599
- if (!token) {
600
- return;
601
- }
602
- const cloudApi = await cloudApiFactory(token);
603
- const { data: config } = await cloudApi.config();
604
- const { questions, defaults: defaultValues } = config.projectCreation;
605
- const projectAnswersDefaulted = defaults(defaultValues);
606
- const projectAnswers = await inquirer.prompt(questions);
607
- const projectInput = projectAnswersDefaulted(projectAnswers);
608
943
  const spinner = logger.spinner("Setting up your project...").start();
609
944
  try {
610
945
  const { data } = await cloudApi.createProject(projectInput);
@@ -612,17 +947,53 @@ const action$3 = async (ctx) => {
612
947
  spinner.succeed("Project created successfully!");
613
948
  return data;
614
949
  } catch (e) {
615
- spinner.fail("Failed to create project on Strapi Cloud.");
616
- await handleError(ctx, e);
950
+ spinner.fail("An error occurred while creating the project on Strapi Cloud.");
951
+ throw e;
617
952
  }
618
- };
619
- function notificationServiceFactory({ logger }) {
620
- return (url, token, cliConfig2) => {
621
- const CONN_TIMEOUT = Number(cliConfig2.notificationsConnectionTimeout);
622
- const es = new EventSource(url, {
623
- headers: {
624
- Authorization: `Bearer ${token}`
625
- }
953
+ }
954
+ const action$6 = async (ctx) => {
955
+ const { logger } = ctx;
956
+ const { getValidToken, eraseToken } = await tokenServiceFactory(ctx);
957
+ const token = await getValidToken(ctx, promptLogin);
958
+ if (!token) {
959
+ return;
960
+ }
961
+ const cloudApi = await cloudApiFactory(ctx, token);
962
+ const { data: config } = await cloudApi.config();
963
+ const projectName = await getProjectNameFromPackageJson(ctx);
964
+ const defaultAnswersMapper = questionDefaultValuesMapper({
965
+ name: projectName,
966
+ nodeVersion: getProjectNodeVersionDefault
967
+ });
968
+ const questions = defaultAnswersMapper(config.projectCreation.questions);
969
+ const defaultValues = {
970
+ ...config.projectCreation.defaults,
971
+ ...getDefaultsFromQuestions(questions)
972
+ };
973
+ const projectAnswersDefaulted = defaults(defaultValues);
974
+ const projectAnswers = await inquirer.prompt(questions);
975
+ const projectInput = projectAnswersDefaulted(projectAnswers);
976
+ try {
977
+ return await createProject$1(ctx, cloudApi, projectInput);
978
+ } catch (e) {
979
+ if (e instanceof AxiosError && e.response?.status === 401) {
980
+ logger.warn("Oops! Your session has expired. Please log in again to retry.");
981
+ await eraseToken();
982
+ if (await promptLogin(ctx)) {
983
+ return await createProject$1(ctx, cloudApi, projectInput);
984
+ }
985
+ } else {
986
+ await handleError(ctx, e);
987
+ }
988
+ }
989
+ };
990
+ function notificationServiceFactory({ logger }) {
991
+ return (url, token, cliConfig2) => {
992
+ const CONN_TIMEOUT = Number(cliConfig2.notificationsConnectionTimeout);
993
+ const es = new EventSource(url, {
994
+ headers: {
995
+ Authorization: `Bearer ${token}`
996
+ }
626
997
  });
627
998
  let timeoutId;
628
999
  const resetTimeout = () => {
@@ -647,38 +1018,6 @@ function notificationServiceFactory({ logger }) {
647
1018
  };
648
1019
  };
649
1020
  }
650
- yup.object({
651
- name: yup.string().required(),
652
- exports: yup.lazy(
653
- (value) => yup.object(
654
- typeof value === "object" ? Object.entries(value).reduce((acc, [key, value2]) => {
655
- if (typeof value2 === "object") {
656
- acc[key] = yup.object({
657
- types: yup.string().optional(),
658
- source: yup.string().required(),
659
- module: yup.string().optional(),
660
- import: yup.string().required(),
661
- require: yup.string().required(),
662
- default: yup.string().required()
663
- }).noUnknown(true);
664
- } else {
665
- acc[key] = yup.string().matches(/^\.\/.*\.json$/).required();
666
- }
667
- return acc;
668
- }, {}) : void 0
669
- ).optional()
670
- )
671
- });
672
- const loadPkg = async ({ cwd, logger }) => {
673
- const pkgPath = await pkgUp({ cwd });
674
- if (!pkgPath) {
675
- throw new Error("Could not find a package.json in the current directory");
676
- }
677
- const buffer = await fs$1.readFile(pkgPath);
678
- const pkg = JSON.parse(buffer.toString());
679
- logger.debug("Loaded package.json:", os.EOL, pkg);
680
- return pkg;
681
- };
682
1021
  const buildLogsServiceFactory = ({ logger }) => {
683
1022
  return async (url, token, cliConfig2) => {
684
1023
  const CONN_TIMEOUT = Number(cliConfig2.buildLogsConnectionTimeout);
@@ -732,6 +1071,7 @@ const buildLogsServiceFactory = ({ logger }) => {
732
1071
  if (retries > MAX_RETRIES) {
733
1072
  spinner.fail("We were unable to connect to the server to get build logs at this time.");
734
1073
  es.close();
1074
+ clearExistingTimeout();
735
1075
  reject(new Error("Max retries reached"));
736
1076
  }
737
1077
  };
@@ -740,8 +1080,31 @@ const buildLogsServiceFactory = ({ logger }) => {
740
1080
  });
741
1081
  };
742
1082
  };
1083
+ const boxenOptions = {
1084
+ padding: 1,
1085
+ margin: 1,
1086
+ align: "center",
1087
+ borderColor: "yellow",
1088
+ borderStyle: "round"
1089
+ };
1090
+ const QUIT_OPTION$2 = "Quit";
1091
+ async function promptForEnvironment(environments) {
1092
+ const choices = environments.map((env2) => ({ name: env2, value: env2 }));
1093
+ const { selectedEnvironment } = await inquirer.prompt([
1094
+ {
1095
+ type: "list",
1096
+ name: "selectedEnvironment",
1097
+ message: "Select the environment to deploy:",
1098
+ choices: [...choices, { name: chalk.grey(`(${QUIT_OPTION$2})`), value: null }]
1099
+ }
1100
+ ]);
1101
+ if (selectedEnvironment === null) {
1102
+ process.exit(1);
1103
+ }
1104
+ return selectedEnvironment;
1105
+ }
743
1106
  async function upload(ctx, project, token, maxProjectFileSize) {
744
- const cloudApi = await cloudApiFactory(token);
1107
+ const cloudApi = await cloudApiFactory(ctx, token);
745
1108
  try {
746
1109
  const storagePath = await getTmpStoragePath();
747
1110
  const projectFolder = path__default.resolve(process.cwd());
@@ -774,13 +1137,13 @@ async function upload(ctx, project, token, maxProjectFileSize) {
774
1137
  process.exit(1);
775
1138
  }
776
1139
  const tarFilePath = path__default.resolve(storagePath, compressedFilename);
777
- const fileStats = await fse.stat(tarFilePath);
1140
+ const fileStats = await fse__default.stat(tarFilePath);
778
1141
  if (fileStats.size > maxProjectFileSize) {
779
1142
  ctx.logger.log(
780
1143
  "Unable to proceed: Your project is too big to be transferred, please use a git repo instead."
781
1144
  );
782
1145
  try {
783
- await fse.remove(tarFilePath);
1146
+ await fse__default.remove(tarFilePath);
784
1147
  } catch (e) {
785
1148
  ctx.logger.log("Unable to remove file: ", tarFilePath);
786
1149
  ctx.logger.debug(e);
@@ -806,20 +1169,10 @@ async function upload(ctx, project, token, maxProjectFileSize) {
806
1169
  return data.build_id;
807
1170
  } catch (e) {
808
1171
  progressBar.stop();
809
- if (e instanceof AxiosError && e.response?.data) {
810
- if (e.response.status === 404) {
811
- ctx.logger.error(
812
- `The project does not exist. Remove the ${LOCAL_SAVE_FILENAME} file and try again.`
813
- );
814
- } else {
815
- ctx.logger.error(e.response.data);
816
- }
817
- } else {
818
- ctx.logger.error("An error occurred while deploying the project. Please try again later.");
819
- }
1172
+ ctx.logger.error("An error occurred while deploying the project. Please try again later.");
820
1173
  ctx.logger.debug(e);
821
1174
  } finally {
822
- await fse.remove(tarFilePath);
1175
+ await fse__default.remove(tarFilePath);
823
1176
  }
824
1177
  process.exit(0);
825
1178
  } catch (e) {
@@ -832,7 +1185,7 @@ async function getProject(ctx) {
832
1185
  const { project } = await retrieve();
833
1186
  if (!project) {
834
1187
  try {
835
- return await action$3(ctx);
1188
+ return await action$6(ctx);
836
1189
  } catch (e) {
837
1190
  ctx.logger.error("An error occurred while deploying the project. Please try again later.");
838
1191
  ctx.logger.debug(e);
@@ -841,10 +1194,47 @@ async function getProject(ctx) {
841
1194
  }
842
1195
  return project;
843
1196
  }
844
- const action$2 = async (ctx) => {
1197
+ async function getConfig({
1198
+ ctx,
1199
+ cloudApiService
1200
+ }) {
1201
+ try {
1202
+ const { data: cliConfig2 } = await cloudApiService.config();
1203
+ return cliConfig2;
1204
+ } catch (e) {
1205
+ ctx.logger.debug("Failed to get cli config", e);
1206
+ return null;
1207
+ }
1208
+ }
1209
+ function validateEnvironment(ctx, environment, environments) {
1210
+ if (!environments.includes(environment)) {
1211
+ ctx.logger.error(`Environment ${environment} does not exist.`);
1212
+ process.exit(1);
1213
+ }
1214
+ }
1215
+ async function getTargetEnvironment(ctx, opts, project, environments) {
1216
+ if (opts.env) {
1217
+ validateEnvironment(ctx, opts.env, environments);
1218
+ return opts.env;
1219
+ }
1220
+ if (project.targetEnvironment) {
1221
+ return project.targetEnvironment;
1222
+ }
1223
+ if (environments.length > 1) {
1224
+ return promptForEnvironment(environments);
1225
+ }
1226
+ return environments[0];
1227
+ }
1228
+ function hasPendingOrLiveDeployment(environments, targetEnvironment) {
1229
+ const environment = environments.find((env2) => env2.name === targetEnvironment);
1230
+ if (!environment) {
1231
+ throw new Error(`Environment details ${targetEnvironment} not found.`);
1232
+ }
1233
+ return environment.hasPendingDeployment || environment.hasLiveDeployment || false;
1234
+ }
1235
+ const action$5 = async (ctx, opts) => {
845
1236
  const { getValidToken } = await tokenServiceFactory(ctx);
846
- const cloudApiService = await cloudApiFactory();
847
- const token = await getValidToken();
1237
+ const token = await getValidToken(ctx, promptLogin);
848
1238
  if (!token) {
849
1239
  return;
850
1240
  }
@@ -852,14 +1242,57 @@ const action$2 = async (ctx) => {
852
1242
  if (!project) {
853
1243
  return;
854
1244
  }
1245
+ const cloudApiService = await cloudApiFactory(ctx, token);
1246
+ let projectData;
1247
+ let environments;
1248
+ let environmentsDetails;
855
1249
  try {
856
- await cloudApiService.track("willDeployWithCLI", { projectInternalName: project.name });
1250
+ const {
1251
+ data: { data, metadata }
1252
+ } = await cloudApiService.getProject({ name: project.name });
1253
+ projectData = data;
1254
+ environments = projectData.environments;
1255
+ environmentsDetails = projectData.environmentsDetails;
1256
+ const isProjectSuspended = projectData.suspendedAt;
1257
+ if (isProjectSuspended) {
1258
+ ctx.logger.log(
1259
+ "\n Oops! This project has been suspended. \n\n Please reactivate it from the dashboard to continue deploying: "
1260
+ );
1261
+ ctx.logger.log(chalk.underline(`${metadata.dashboardUrls.project}`));
1262
+ return;
1263
+ }
857
1264
  } catch (e) {
858
- ctx.logger.debug("Failed to track willDeploy", e);
1265
+ if (e instanceof AxiosError && e.response?.data) {
1266
+ if (e.response.status === 404) {
1267
+ ctx.logger.warn(
1268
+ `The project associated with this folder does not exist in Strapi Cloud.
1269
+ Please link your local project to an existing Strapi Cloud project using the ${chalk.cyan(
1270
+ "link"
1271
+ )} command before deploying.`
1272
+ );
1273
+ } else {
1274
+ ctx.logger.error(e.response.data);
1275
+ }
1276
+ } else {
1277
+ ctx.logger.error(
1278
+ "An error occurred while retrieving the project's information. Please try again later."
1279
+ );
1280
+ }
1281
+ ctx.logger.debug(e);
1282
+ return;
859
1283
  }
1284
+ await trackEvent(ctx, cloudApiService, "willDeployWithCLI", {
1285
+ projectInternalName: project.name
1286
+ });
860
1287
  const notificationService = notificationServiceFactory(ctx);
861
1288
  const buildLogsService = buildLogsServiceFactory(ctx);
862
- const { data: cliConfig2 } = await cloudApiService.config();
1289
+ const cliConfig2 = await getConfig({ ctx, cloudApiService });
1290
+ if (!cliConfig2) {
1291
+ ctx.logger.error(
1292
+ "An error occurred while retrieving data from Strapi Cloud. Please check your network or try again later."
1293
+ );
1294
+ return;
1295
+ }
863
1296
  let maxSize = parseInt(cliConfig2.maxProjectFileSize, 10);
864
1297
  if (Number.isNaN(maxSize)) {
865
1298
  ctx.logger.debug(
@@ -867,11 +1300,34 @@ const action$2 = async (ctx) => {
867
1300
  );
868
1301
  maxSize = 1e8;
869
1302
  }
1303
+ project.targetEnvironment = await getTargetEnvironment(ctx, opts, project, environments);
1304
+ if (!opts.force) {
1305
+ const shouldDisplayWarning = hasPendingOrLiveDeployment(
1306
+ environmentsDetails,
1307
+ project.targetEnvironment
1308
+ );
1309
+ if (shouldDisplayWarning) {
1310
+ ctx.logger.log(boxen(cliConfig2.projectDeployment.confirmationText, boxenOptions));
1311
+ const { confirm } = await inquirer.prompt([
1312
+ {
1313
+ type: "confirm",
1314
+ name: "confirm",
1315
+ message: `Do you want to proceed with deployment to ${chalk.cyan(projectData.displayName)} on ${chalk.cyan(project.targetEnvironment)} environment?`
1316
+ }
1317
+ ]);
1318
+ if (!confirm) {
1319
+ process.exit(1);
1320
+ }
1321
+ }
1322
+ }
870
1323
  const buildId = await upload(ctx, project, token, maxSize);
871
1324
  if (!buildId) {
872
1325
  return;
873
1326
  }
874
1327
  try {
1328
+ ctx.logger.log(
1329
+ `🚀 Deploying project to ${chalk.cyan(project.targetEnvironment ?? `production`)} environment...`
1330
+ );
875
1331
  notificationService(`${apiConfig.apiBaseUrl}/notifications`, token, cliConfig2);
876
1332
  await buildLogsService(`${apiConfig.apiBaseUrl}/v1/logs/${buildId}`, token, cliConfig2);
877
1333
  ctx.logger.log(
@@ -881,10 +1337,11 @@ const action$2 = async (ctx) => {
881
1337
  chalk.underline(`${apiConfig.dashboardBaseUrl}/projects/${project.name}/deployments`)
882
1338
  );
883
1339
  } catch (e) {
1340
+ ctx.logger.debug(e);
884
1341
  if (e instanceof Error) {
885
1342
  ctx.logger.error(e.message);
886
1343
  } else {
887
- throw e;
1344
+ ctx.logger.error("An error occurred while deploying the project. Please try again later.");
888
1345
  }
889
1346
  }
890
1347
  };
@@ -915,185 +1372,191 @@ const runAction = (name2, action2) => (...args) => {
915
1372
  process.exit(1);
916
1373
  });
917
1374
  };
918
- const command$3 = ({ command: command2, ctx }) => {
919
- 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$2)(ctx));
1375
+ const command$7 = ({ ctx }) => {
1376
+ return createCommand("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").option("-f, --force", "Skip confirmation to deploy").option("-e, --env <name>", "Specify the environment to deploy").action((opts) => runAction("deploy", action$5)(ctx, opts));
920
1377
  };
921
1378
  const deployProject = {
922
1379
  name: "deploy-project",
923
1380
  description: "Deploy a Strapi Cloud project",
924
- action: action$2,
925
- command: command$3
1381
+ action: action$5,
1382
+ command: command$7
926
1383
  };
927
- const openModule = import("open");
928
- const action$1 = async (ctx) => {
929
- const { logger } = ctx;
930
- const tokenService = await tokenServiceFactory(ctx);
931
- const existingToken = await tokenService.retrieveToken();
932
- const cloudApiService = await cloudApiFactory(existingToken || void 0);
933
- const trackFailedLogin = async () => {
934
- try {
935
- await cloudApiService.track("didNotLogin", { loginMethod: "cli" });
936
- } catch (e) {
937
- logger.debug("Failed to track failed login", e);
938
- }
939
- };
940
- if (existingToken) {
941
- const isTokenValid = await tokenService.isTokenValid(existingToken);
942
- if (isTokenValid) {
943
- try {
944
- const userInfo = await cloudApiService.getUserInfo();
945
- const { email } = userInfo.data.data;
946
- if (email) {
947
- logger.log(`You are already logged into your account (${email}).`);
948
- } else {
949
- logger.log("You are already logged in.");
950
- }
951
- logger.log(
952
- "To access your dashboard, please copy and paste the following URL into your web browser:"
953
- );
954
- logger.log(chalk.underline(`${apiConfig.dashboardBaseUrl}/projects`));
955
- return true;
956
- } catch (e) {
957
- logger.debug("Failed to fetch user info", e);
1384
+ async function getLocalConfig(ctx) {
1385
+ try {
1386
+ return await retrieve();
1387
+ } catch (e) {
1388
+ ctx.logger.debug("Failed to get project config", e);
1389
+ ctx.logger.error("An error occurred while retrieving config data from your local project.");
1390
+ return null;
1391
+ }
1392
+ }
1393
+ async function getLocalProject(ctx) {
1394
+ const localConfig = await getLocalConfig(ctx);
1395
+ if (!localConfig || !localConfig.project) {
1396
+ ctx.logger.warn(
1397
+ `
1398
+ We couldn't find a valid local project config.
1399
+ Please link your local project to an existing Strapi Cloud project using the ${chalk.cyan(
1400
+ "link"
1401
+ )} command.`
1402
+ );
1403
+ process.exit(1);
1404
+ }
1405
+ return localConfig.project;
1406
+ }
1407
+ const QUIT_OPTION$1 = "Quit";
1408
+ async function promptForRelink(ctx, cloudApiService, existingConfig) {
1409
+ if (existingConfig && existingConfig.project) {
1410
+ const { shouldRelink } = await inquirer.prompt([
1411
+ {
1412
+ type: "confirm",
1413
+ name: "shouldRelink",
1414
+ message: `A project named ${chalk.cyan(
1415
+ existingConfig.project.displayName ? existingConfig.project.displayName : existingConfig.project.name
1416
+ )} is already linked to this local folder. Do you want to update the link?`,
1417
+ default: false
958
1418
  }
1419
+ ]);
1420
+ if (!shouldRelink) {
1421
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1422
+ currentProjectName: existingConfig.project?.name
1423
+ });
1424
+ return false;
959
1425
  }
960
1426
  }
961
- let cliConfig2;
1427
+ return true;
1428
+ }
1429
+ async function getProjectsList(ctx, cloudApiService, existingConfig) {
1430
+ const spinner = ctx.logger.spinner("Fetching your projects...\n").start();
962
1431
  try {
963
- logger.info("🔌 Connecting to the Strapi Cloud API...");
964
- const config = await cloudApiService.config();
965
- cliConfig2 = config.data;
1432
+ const {
1433
+ data: { data: projectList }
1434
+ } = await cloudApiService.listLinkProjects();
1435
+ spinner.succeed();
1436
+ if (!Array.isArray(projectList)) {
1437
+ ctx.logger.log("We couldn't find any projects available for linking in Strapi Cloud.");
1438
+ return null;
1439
+ }
1440
+ const projects = projectList.filter(
1441
+ (project) => !(project.isMaintainer || project.name === existingConfig?.project?.name)
1442
+ ).map((project) => {
1443
+ return {
1444
+ name: project.displayName,
1445
+ value: { name: project.name, displayName: project.displayName }
1446
+ };
1447
+ });
1448
+ if (projects.length === 0) {
1449
+ ctx.logger.log("We couldn't find any projects available for linking in Strapi Cloud.");
1450
+ return null;
1451
+ }
1452
+ return projects;
966
1453
  } catch (e) {
967
- logger.error("🥲 Oops! Something went wrong while logging you in. Please try again.");
968
- logger.debug(e);
969
- return false;
1454
+ spinner.fail("An error occurred while fetching your projects from Strapi Cloud.");
1455
+ ctx.logger.debug("Failed to list projects", e);
1456
+ return null;
970
1457
  }
1458
+ }
1459
+ async function getUserSelection(ctx, projects) {
1460
+ const { logger } = ctx;
971
1461
  try {
972
- await cloudApiService.track("willLoginAttempt", {});
1462
+ const answer = await inquirer.prompt([
1463
+ {
1464
+ type: "list",
1465
+ name: "linkProject",
1466
+ message: "Which project do you want to link?",
1467
+ choices: [...projects, { name: chalk.grey(`(${QUIT_OPTION$1})`), value: null }]
1468
+ }
1469
+ ]);
1470
+ if (!answer.linkProject) {
1471
+ return null;
1472
+ }
1473
+ return answer;
973
1474
  } catch (e) {
974
- logger.debug("Failed to track login attempt", e);
1475
+ logger.debug("Failed to get user input", e);
1476
+ logger.error("An error occurred while trying to get your input.");
1477
+ return null;
975
1478
  }
976
- logger.debug("🔐 Creating device authentication request...", {
977
- client_id: cliConfig2.clientId,
978
- scope: cliConfig2.scope,
979
- audience: cliConfig2.audience
980
- });
981
- const deviceAuthResponse = await axios.post(cliConfig2.deviceCodeAuthUrl, {
982
- client_id: cliConfig2.clientId,
983
- scope: cliConfig2.scope,
984
- audience: cliConfig2.audience
985
- }).catch((e) => {
986
- logger.error("There was an issue with the authentication process. Please try again.");
987
- if (e.message) {
988
- logger.debug(e.message, e);
989
- } else {
990
- logger.debug(e);
991
- }
992
- });
993
- openModule.then((open) => {
994
- open.default(deviceAuthResponse.data.verification_uri_complete).catch((e) => {
995
- logger.error("We encountered an issue opening the browser. Please try again later.");
996
- logger.debug(e.message, e);
997
- });
998
- });
999
- logger.log("If a browser tab does not open automatically, please follow the next steps:");
1000
- logger.log(
1001
- `1. Open this url in your device: ${deviceAuthResponse.data.verification_uri_complete}`
1002
- );
1003
- logger.log(
1004
- `2. Enter the following code: ${deviceAuthResponse.data.user_code} and confirm to login.
1005
- `
1479
+ }
1480
+ const action$4 = async (ctx) => {
1481
+ const { getValidToken } = await tokenServiceFactory(ctx);
1482
+ const token = await getValidToken(ctx, promptLogin);
1483
+ const { logger } = ctx;
1484
+ if (!token) {
1485
+ return;
1486
+ }
1487
+ const cloudApiService = await cloudApiFactory(ctx, token);
1488
+ const existingConfig = await getLocalConfig(ctx);
1489
+ const shouldRelink = await promptForRelink(ctx, cloudApiService, existingConfig);
1490
+ if (!shouldRelink) {
1491
+ return;
1492
+ }
1493
+ await trackEvent(ctx, cloudApiService, "willLinkProject", {});
1494
+ const projects = await getProjectsList(
1495
+ ctx,
1496
+ cloudApiService,
1497
+ existingConfig
1006
1498
  );
1007
- const tokenPayload = {
1008
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1009
- device_code: deviceAuthResponse.data.device_code,
1010
- client_id: cliConfig2.clientId
1011
- };
1012
- let isAuthenticated = false;
1013
- const authenticate = async () => {
1014
- const spinner = logger.spinner("Waiting for authentication");
1015
- spinner.start();
1016
- const spinnerFail = () => spinner.fail("Authentication failed!");
1017
- while (!isAuthenticated) {
1018
- try {
1019
- const tokenResponse = await axios.post(cliConfig2.tokenUrl, tokenPayload);
1020
- const authTokenData = tokenResponse.data;
1021
- if (tokenResponse.status === 200) {
1022
- try {
1023
- logger.debug("🔐 Validating token...");
1024
- await tokenService.validateToken(authTokenData.id_token, cliConfig2.jwksUrl);
1025
- logger.debug("🔐 Token validation successful!");
1026
- } catch (e) {
1027
- logger.debug(e);
1028
- spinnerFail();
1029
- throw new Error("Unable to proceed: Token validation failed");
1030
- }
1031
- logger.debug("🔍 Fetching user information...");
1032
- const cloudApiServiceWithToken = await cloudApiFactory(authTokenData.access_token);
1033
- await cloudApiServiceWithToken.getUserInfo();
1034
- logger.debug("🔍 User information fetched successfully!");
1035
- try {
1036
- logger.debug("📝 Saving login information...");
1037
- await tokenService.saveToken(authTokenData.access_token);
1038
- logger.debug("📝 Login information saved successfully!");
1039
- isAuthenticated = true;
1040
- } catch (e) {
1041
- logger.error(
1042
- "There was a problem saving your login information. Please try logging in again."
1043
- );
1044
- logger.debug(e);
1045
- spinnerFail();
1046
- return false;
1047
- }
1048
- }
1049
- } catch (e) {
1050
- if (e.message === "Unable to proceed: Token validation failed") {
1051
- logger.error(
1052
- "There seems to be a problem with your login information. Please try logging in again."
1053
- );
1054
- spinnerFail();
1055
- await trackFailedLogin();
1056
- return false;
1057
- }
1058
- if (e.response?.data.error && !["authorization_pending", "slow_down"].includes(e.response.data.error)) {
1059
- logger.debug(e);
1060
- spinnerFail();
1061
- await trackFailedLogin();
1062
- return false;
1063
- }
1064
- await new Promise((resolve) => {
1065
- setTimeout(resolve, deviceAuthResponse.data.interval * 1e3);
1066
- });
1499
+ if (!projects) {
1500
+ return;
1501
+ }
1502
+ const answer = await getUserSelection(ctx, projects);
1503
+ if (!answer) {
1504
+ return;
1505
+ }
1506
+ try {
1507
+ const { confirmAction } = await inquirer.prompt([
1508
+ {
1509
+ type: "confirm",
1510
+ name: "confirmAction",
1511
+ message: "Warning: Once linked, deploying from CLI will replace the existing project and its data. Confirm to proceed:",
1512
+ default: false
1067
1513
  }
1514
+ ]);
1515
+ if (!confirmAction) {
1516
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1517
+ cancelledProjectName: answer.linkProject.name,
1518
+ currentProjectName: existingConfig ? existingConfig.project?.name : null
1519
+ });
1520
+ return;
1068
1521
  }
1069
- spinner.succeed("Authentication successful!");
1070
- logger.log("You are now logged into Strapi Cloud.");
1522
+ await save({ project: answer.linkProject });
1071
1523
  logger.log(
1072
- "To access your dashboard, please copy and paste the following URL into your web browser:"
1524
+ ` You have successfully linked your project to ${chalk.cyan(answer.linkProject.displayName)}. You are now able to deploy your project.`
1073
1525
  );
1074
- logger.log(chalk.underline(`${apiConfig.dashboardBaseUrl}/projects`));
1075
- try {
1076
- await cloudApiService.track("didLogin", { loginMethod: "cli" });
1077
- } catch (e) {
1078
- logger.debug("Failed to track login", e);
1079
- }
1080
- };
1081
- await authenticate();
1082
- return isAuthenticated;
1526
+ await trackEvent(ctx, cloudApiService, "didLinkProject", {
1527
+ projectInternalName: answer.linkProject
1528
+ });
1529
+ } catch (e) {
1530
+ logger.debug("Failed to link project", e);
1531
+ logger.error("An error occurred while linking the project.");
1532
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1533
+ projectInternalName: answer.linkProject
1534
+ });
1535
+ }
1083
1536
  };
1084
- const command$2 = ({ command: command2, ctx }) => {
1085
- command2.command("cloud:login").alias("login").description("Strapi Cloud Login").addHelpText(
1537
+ const command$6 = ({ command: command2, ctx }) => {
1538
+ 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$4)(ctx));
1539
+ };
1540
+ const link = {
1541
+ name: "link-project",
1542
+ description: "Link a local directory to a Strapi Cloud project",
1543
+ action: action$4,
1544
+ command: command$6
1545
+ };
1546
+ const command$5 = ({ ctx }) => {
1547
+ return createCommand("cloud:login").alias("login").description("Strapi Cloud Login").addHelpText(
1086
1548
  "after",
1087
1549
  "\nAfter running this command, you will be prompted to enter your authentication information."
1088
- ).option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("login", action$1)(ctx));
1550
+ ).option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("login", loginAction)(ctx));
1089
1551
  };
1090
1552
  const login = {
1091
1553
  name: "login",
1092
1554
  description: "Strapi Cloud Login",
1093
- action: action$1,
1094
- command: command$2
1555
+ action: loginAction,
1556
+ command: command$5
1095
1557
  };
1096
- const action = async (ctx) => {
1558
+ const openModule = import("open");
1559
+ const action$3 = async (ctx) => {
1097
1560
  const { logger } = ctx;
1098
1561
  const { retrieveToken, eraseToken } = await tokenServiceFactory(ctx);
1099
1562
  const token = await retrieveToken();
@@ -1101,9 +1564,21 @@ const action = async (ctx) => {
1101
1564
  logger.log("You're already logged out.");
1102
1565
  return;
1103
1566
  }
1104
- const cloudApiService = await cloudApiFactory(token);
1567
+ const cloudApiService = await cloudApiFactory(ctx, token);
1568
+ const config = await cloudApiService.config();
1569
+ const cliConfig2 = config.data;
1105
1570
  try {
1106
1571
  await eraseToken();
1572
+ openModule.then((open) => {
1573
+ open.default(
1574
+ `${cliConfig2.baseUrl}/oidc/logout?client_id=${encodeURIComponent(
1575
+ cliConfig2.clientId
1576
+ )}&logout_hint=${encodeURIComponent(token)}
1577
+ `
1578
+ ).catch((e) => {
1579
+ logger.debug(e.message, e);
1580
+ });
1581
+ });
1107
1582
  logger.log(
1108
1583
  "🔌 You have been logged out from the CLI. If you are on a shared computer, please make sure to log out from the Strapi Cloud Dashboard as well."
1109
1584
  );
@@ -1111,39 +1586,258 @@ const action = async (ctx) => {
1111
1586
  logger.error("🥲 Oops! Something went wrong while logging you out. Please try again.");
1112
1587
  logger.debug(e);
1113
1588
  }
1114
- try {
1115
- await cloudApiService.track("didLogout", { loginMethod: "cli" });
1116
- } catch (e) {
1117
- logger.debug("Failed to track logout event", e);
1118
- }
1589
+ await trackEvent(ctx, cloudApiService, "didLogout", { loginMethod: "cli" });
1119
1590
  };
1120
- const command$1 = ({ command: command2, ctx }) => {
1121
- 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));
1591
+ const command$4 = ({ ctx }) => {
1592
+ return createCommand("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$3)(ctx));
1122
1593
  };
1123
1594
  const logout = {
1124
1595
  name: "logout",
1125
1596
  description: "Strapi Cloud Logout",
1126
- action,
1127
- command: command$1
1597
+ action: action$3,
1598
+ command: command$4
1128
1599
  };
1129
- const command = ({ command: command2, ctx }) => {
1130
- 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$3)(ctx));
1600
+ const command$3 = ({ ctx }) => {
1601
+ return createCommand("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$6)(ctx));
1131
1602
  };
1132
1603
  const createProject = {
1133
1604
  name: "create-project",
1134
1605
  description: "Create a new project",
1135
- action: action$3,
1606
+ action: action$6,
1607
+ command: command$3
1608
+ };
1609
+ const action$2 = async (ctx) => {
1610
+ const { getValidToken } = await tokenServiceFactory(ctx);
1611
+ const token = await getValidToken(ctx, promptLogin);
1612
+ const { logger } = ctx;
1613
+ if (!token) {
1614
+ return;
1615
+ }
1616
+ const cloudApiService = await cloudApiFactory(ctx, token);
1617
+ const spinner = logger.spinner("Fetching your projects...").start();
1618
+ try {
1619
+ const {
1620
+ data: { data: projectList }
1621
+ } = await cloudApiService.listProjects();
1622
+ spinner.succeed();
1623
+ logger.log(projectList);
1624
+ } catch (e) {
1625
+ ctx.logger.debug("Failed to list projects", e);
1626
+ spinner.fail("An error occurred while fetching your projects from Strapi Cloud.");
1627
+ }
1628
+ };
1629
+ const command$2 = ({ command: command2, ctx }) => {
1630
+ 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$2)(ctx));
1631
+ };
1632
+ const listProjects = {
1633
+ name: "list-projects",
1634
+ description: "List Strapi Cloud projects",
1635
+ action: action$2,
1636
+ command: command$2
1637
+ };
1638
+ const action$1 = async (ctx) => {
1639
+ const { getValidToken } = await tokenServiceFactory(ctx);
1640
+ const token = await getValidToken(ctx, promptLogin);
1641
+ const { logger } = ctx;
1642
+ if (!token) {
1643
+ return;
1644
+ }
1645
+ const project = await getLocalProject(ctx);
1646
+ if (!project) {
1647
+ ctx.logger.debug(`No valid local project configuration was found.`);
1648
+ return;
1649
+ }
1650
+ const cloudApiService = await cloudApiFactory(ctx, token);
1651
+ const spinner = logger.spinner("Fetching environments...").start();
1652
+ await trackEvent(ctx, cloudApiService, "willListEnvironment", {
1653
+ projectInternalName: project.name
1654
+ });
1655
+ try {
1656
+ const {
1657
+ data: { data: environmentsList }
1658
+ } = await cloudApiService.listEnvironments({ name: project.name });
1659
+ spinner.succeed();
1660
+ logger.log(environmentsList);
1661
+ await trackEvent(ctx, cloudApiService, "didListEnvironment", {
1662
+ projectInternalName: project.name
1663
+ });
1664
+ } catch (e) {
1665
+ if (e.response && e.response.status === 404) {
1666
+ spinner.succeed();
1667
+ logger.warn(
1668
+ `
1669
+ The project associated with this folder does not exist in Strapi Cloud.
1670
+ Please link your local project to an existing Strapi Cloud project using the ${chalk.cyan(
1671
+ "link"
1672
+ )} command`
1673
+ );
1674
+ } else {
1675
+ spinner.fail("An error occurred while fetching environments data from Strapi Cloud.");
1676
+ logger.debug("Failed to list environments", e);
1677
+ }
1678
+ await trackEvent(ctx, cloudApiService, "didNotListEnvironment", {
1679
+ projectInternalName: project.name
1680
+ });
1681
+ }
1682
+ };
1683
+ function defineCloudNamespace(command2, ctx) {
1684
+ const cloud = command2.command("cloud").description("Manage Strapi Cloud projects");
1685
+ cloud.command("environments").description("Alias for cloud environment list").action(() => runAction("list", action$1)(ctx));
1686
+ return cloud;
1687
+ }
1688
+ let environmentCmd = null;
1689
+ const initializeEnvironmentCommand = (command2, ctx) => {
1690
+ if (!environmentCmd) {
1691
+ const cloud = defineCloudNamespace(command2, ctx);
1692
+ environmentCmd = cloud.command("environment").description("Manage environments");
1693
+ }
1694
+ return environmentCmd;
1695
+ };
1696
+ const command$1 = ({ command: command2, ctx }) => {
1697
+ const environmentCmd2 = initializeEnvironmentCommand(command2, ctx);
1698
+ environmentCmd2.command("list").description("List Strapi Cloud project environments").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("list", action$1)(ctx));
1699
+ };
1700
+ const listEnvironments = {
1701
+ name: "list-environments",
1702
+ description: "List Strapi Cloud environments",
1703
+ action: action$1,
1704
+ command: command$1
1705
+ };
1706
+ const QUIT_OPTION = "Quit";
1707
+ const action = async (ctx) => {
1708
+ const { getValidToken } = await tokenServiceFactory(ctx);
1709
+ const token = await getValidToken(ctx, promptLogin);
1710
+ const { logger } = ctx;
1711
+ if (!token) {
1712
+ return;
1713
+ }
1714
+ const project = await getLocalProject(ctx);
1715
+ if (!project) {
1716
+ logger.debug(`No valid local project configuration was found.`);
1717
+ return;
1718
+ }
1719
+ const cloudApiService = await cloudApiFactory(ctx, token);
1720
+ const environments = await getEnvironmentsList(ctx, cloudApiService, project);
1721
+ if (!environments) {
1722
+ logger.debug(`Fetching environments failed.`);
1723
+ return;
1724
+ }
1725
+ if (environments.length === 0) {
1726
+ logger.log(
1727
+ `The only available environment is already linked. You can add a new one from your project settings on the Strapi Cloud dashboard.`
1728
+ );
1729
+ return;
1730
+ }
1731
+ const answer = await promptUserForEnvironment(ctx, environments);
1732
+ if (!answer) {
1733
+ return;
1734
+ }
1735
+ await trackEvent(ctx, cloudApiService, "willLinkEnvironment", {
1736
+ projectName: project.name,
1737
+ environmentName: answer.targetEnvironment
1738
+ });
1739
+ try {
1740
+ await patch({ project: { targetEnvironment: answer.targetEnvironment } });
1741
+ } catch (e) {
1742
+ await trackEvent(ctx, cloudApiService, "didNotLinkEnvironment", {
1743
+ projectName: project.name,
1744
+ environmentName: answer.targetEnvironment
1745
+ });
1746
+ logger.debug("Failed to link environment", e);
1747
+ logger.error(
1748
+ "Failed to link the environment. If this issue persists, try re-linking your project or contact support."
1749
+ );
1750
+ process.exit(1);
1751
+ }
1752
+ logger.log(
1753
+ ` You have successfully linked your project to ${chalk.cyan(answer.targetEnvironment)}, on ${chalk.cyan(project.displayName)}. You are now able to deploy your project.`
1754
+ );
1755
+ await trackEvent(ctx, cloudApiService, "didLinkEnvironment", {
1756
+ projectName: project.name,
1757
+ environmentName: answer.targetEnvironment
1758
+ });
1759
+ };
1760
+ async function promptUserForEnvironment(ctx, environments) {
1761
+ const { logger } = ctx;
1762
+ try {
1763
+ const answer = await inquirer.prompt([
1764
+ {
1765
+ type: "list",
1766
+ name: "targetEnvironment",
1767
+ message: "Which environment do you want to link?",
1768
+ choices: [...environments, { name: chalk.grey(`(${QUIT_OPTION})`), value: null }]
1769
+ }
1770
+ ]);
1771
+ if (!answer.targetEnvironment) {
1772
+ return null;
1773
+ }
1774
+ return answer;
1775
+ } catch (e) {
1776
+ logger.debug("Failed to get user input", e);
1777
+ logger.error("An error occurred while trying to get your environment selection.");
1778
+ return null;
1779
+ }
1780
+ }
1781
+ async function getEnvironmentsList(ctx, cloudApiService, project) {
1782
+ const spinner = ctx.logger.spinner("Fetching environments...\n").start();
1783
+ try {
1784
+ const {
1785
+ data: { data: environmentsList }
1786
+ } = await cloudApiService.listLinkEnvironments({ name: project.name });
1787
+ if (!Array.isArray(environmentsList) || environmentsList.length === 0) {
1788
+ throw new Error("Environments not found in server response");
1789
+ }
1790
+ spinner.succeed();
1791
+ return environmentsList.filter(
1792
+ (environment) => environment.name !== project.targetEnvironment
1793
+ );
1794
+ } catch (e) {
1795
+ if (e.response && e.response.status === 404) {
1796
+ spinner.succeed();
1797
+ ctx.logger.warn(
1798
+ `
1799
+ The project associated with this folder does not exist in Strapi Cloud.
1800
+ Please link your local project to an existing Strapi Cloud project using the ${chalk.cyan(
1801
+ "link"
1802
+ )} command.`
1803
+ );
1804
+ } else {
1805
+ spinner.fail("An error occurred while fetching environments data from Strapi Cloud.");
1806
+ ctx.logger.debug("Failed to list environments", e);
1807
+ }
1808
+ }
1809
+ }
1810
+ const command = ({ command: command2, ctx }) => {
1811
+ const environmentCmd2 = initializeEnvironmentCommand(command2, ctx);
1812
+ environmentCmd2.command("link").description("Link project to a specific Strapi Cloud project environment").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("link", action)(ctx));
1813
+ };
1814
+ const linkEnvironment = {
1815
+ name: "link-environment",
1816
+ description: "Link Strapi Cloud environment to a local project",
1817
+ action,
1136
1818
  command
1137
1819
  };
1138
1820
  const cli = {
1139
1821
  deployProject,
1822
+ link,
1140
1823
  login,
1141
1824
  logout,
1142
- createProject
1825
+ createProject,
1826
+ linkEnvironment,
1827
+ listProjects,
1828
+ listEnvironments
1143
1829
  };
1144
- const cloudCommands = [deployProject, login, logout];
1830
+ const cloudCommands = [
1831
+ deployProject,
1832
+ link,
1833
+ login,
1834
+ logout,
1835
+ linkEnvironment,
1836
+ listProjects,
1837
+ listEnvironments
1838
+ ];
1145
1839
  async function initCloudCLIConfig() {
1146
- const localConfig = await getLocalConfig();
1840
+ const localConfig = await getLocalConfig$1();
1147
1841
  if (!localConfig.deviceId) {
1148
1842
  localConfig.deviceId = crypto$1.randomUUID();
1149
1843
  }
@@ -1157,7 +1851,10 @@ async function buildStrapiCloudCommands({
1157
1851
  await initCloudCLIConfig();
1158
1852
  for (const cloudCommand of cloudCommands) {
1159
1853
  try {
1160
- await cloudCommand.command({ command: command2, ctx, argv });
1854
+ const subCommand = await cloudCommand.command({ command: command2, ctx, argv });
1855
+ if (subCommand) {
1856
+ command2.addCommand(subCommand);
1857
+ }
1161
1858
  } catch (e) {
1162
1859
  console.error(`Failed to load command ${cloudCommand.name}`, e);
1163
1860
  }