@strapi/cloud-cli 0.0.0-next.b11829e6c6aacb45bc1ec2f341609d04d88080d2 → 0.0.0-next.b6435ada233136a0d0b14fba67961ff8f16cdac2

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