diff --git a/packages/cli/cloud/src/cloud/command.ts b/packages/cli/cloud/src/cloud/command.ts new file mode 100644 index 0000000000..c47cb5f7a4 --- /dev/null +++ b/packages/cli/cloud/src/cloud/command.ts @@ -0,0 +1,5 @@ +import { Command } from 'commander'; + +export function defineCloudNamespace(command: Command): Command { + return command.command('cloud').description('Manage Strapi Cloud projects'); +} diff --git a/packages/cli/cloud/src/environment/command.ts b/packages/cli/cloud/src/environment/command.ts new file mode 100644 index 0000000000..11f0309dca --- /dev/null +++ b/packages/cli/cloud/src/environment/command.ts @@ -0,0 +1,7 @@ +import { Command } from 'commander'; +import { defineCloudNamespace } from '../cloud/command'; + +export function createEnvironmentCommand(command: Command): Command { + const cloud = defineCloudNamespace(command); + return cloud.command('environment').description('Manage environments for a Strapi Cloud project'); +} diff --git a/packages/cli/cloud/src/environment/list/action.ts b/packages/cli/cloud/src/environment/list/action.ts new file mode 100644 index 0000000000..02fe6a22af --- /dev/null +++ b/packages/cli/cloud/src/environment/list/action.ts @@ -0,0 +1,66 @@ +import chalk from 'chalk'; +import type { CLIContext } from '../../types'; +import { cloudApiFactory, local, tokenServiceFactory } from '../../services'; +import { promptLogin } from '../../login/action'; +import { trackEvent } from '../../utils/analytics'; + +async function getProject(ctx: CLIContext) { + const { project } = await local.retrieve(); + if (!project) { + ctx.logger.warn( + `\nWe couldn't find a valid local project config.\nPlease link your local project to an existing Strapi Cloud project using the ${chalk.cyan( + 'link' + )} command` + ); + process.exit(1); + } + return project; +} + +export default async (ctx: CLIContext) => { + const { getValidToken } = await tokenServiceFactory(ctx); + const token = await getValidToken(ctx, promptLogin); + const { logger } = ctx; + + if (!token) { + return; + } + + const project = await getProject(ctx); + if (!project) { + ctx.logger.debug(`No valid local project configuration was found.`); + return; + } + + const cloudApiService = await cloudApiFactory(ctx, token); + const spinner = logger.spinner('Fetching environments...').start(); + await trackEvent(ctx, cloudApiService, 'willListEnvironment', { + projectInternalName: project.name, + }); + + try { + const { + data: { data: environmentsList }, + } = await cloudApiService.listEnvironments({ name: project.name }); + spinner.succeed(); + logger.log(environmentsList); + await trackEvent(ctx, cloudApiService, 'didListEnvironment', { + projectInternalName: project.name, + }); + } catch (e: any) { + if (e.response && e.response.status === 404) { + spinner.succeed(); + logger.warn( + `\nThe project associated with this folder does not exist in Strapi Cloud. \nPlease link your local project to an existing Strapi Cloud project using the ${chalk.cyan( + 'link' + )} command` + ); + } else { + spinner.fail('An error occurred while fetching environments data from Strapi Cloud.'); + logger.debug('Failed to list environments', e); + } + await trackEvent(ctx, cloudApiService, 'didNotListEnvironment', { + projectInternalName: project.name, + }); + } +}; diff --git a/packages/cli/cloud/src/environment/list/command.ts b/packages/cli/cloud/src/environment/list/command.ts new file mode 100644 index 0000000000..39d3ceca0c --- /dev/null +++ b/packages/cli/cloud/src/environment/list/command.ts @@ -0,0 +1,25 @@ +import { type StrapiCloudCommand } from '../../types'; +import { runAction } from '../../utils/helpers'; +import action from './action'; +import { defineCloudNamespace } from '../../cloud/command'; + +const command: StrapiCloudCommand = ({ command, ctx }) => { + const cloud = defineCloudNamespace(command); + + cloud + .command('environments') + .description('Alias for cloud environment list') + .action(() => runAction('list', action)(ctx)); + + const environment = cloud + .command('environment') + .description('Manage environments for a Strapi Cloud project'); + environment + .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)(ctx)); +}; + +export default command; diff --git a/packages/cli/cloud/src/environment/list/index.ts b/packages/cli/cloud/src/environment/list/index.ts new file mode 100644 index 0000000000..43aaecba82 --- /dev/null +++ b/packages/cli/cloud/src/environment/list/index.ts @@ -0,0 +1,12 @@ +import action from './action'; +import command from './command'; +import type { StrapiCloudCommandInfo } from '../../types'; + +export { action, command }; + +export default { + name: 'list-environments', + description: 'List Strapi Cloud environments', + action, + command, +} as StrapiCloudCommandInfo; diff --git a/packages/cli/cloud/src/index.ts b/packages/cli/cloud/src/index.ts index d25170e8c9..ac05fe9118 100644 --- a/packages/cli/cloud/src/index.ts +++ b/packages/cli/cloud/src/index.ts @@ -6,6 +6,7 @@ import login from './login'; import logout from './logout'; import createProject from './create-project'; import listProjects from './list-projects'; +import listEnvironments from './environment/list'; import { CLIContext } from './types'; import { getLocalConfig, saveLocalConfig } from './config/local'; @@ -16,9 +17,10 @@ export const cli = { logout, createProject, listProjects, + listEnvironments, }; -const cloudCommands = [deployProject, link, login, logout, listProjects]; +const cloudCommands = [deployProject, link, login, logout, listProjects, listEnvironments]; async function initCloudCLIConfig() { const localConfig = await getLocalConfig(); diff --git a/packages/cli/cloud/src/services/cli-api.ts b/packages/cli/cloud/src/services/cli-api.ts index d17f802b88..bea431ed08 100644 --- a/packages/cli/cloud/src/services/cli-api.ts +++ b/packages/cli/cloud/src/services/cli-api.ts @@ -19,6 +19,8 @@ export type ProjectInfos = { url?: string; }; +export type EnvironmentInfo = Record; + export type ProjectInput = Omit; export type DeployResponse = { @@ -32,6 +34,12 @@ export type ListProjectsResponse = { }; }; +export type ListEnvironmentsResponse = { + data: { + data: EnvironmentInfo[] | Record; + }; +}; + export type ListLinkProjectsResponse = { data: { data: ProjectInfos[] | Record; @@ -78,6 +86,8 @@ export interface CloudApiService { listLinkProjects(): Promise>; + listEnvironments(project: { name: string }): Promise>; + getProject(project: { name: string }): Promise>; track(event: string, payload?: TrackPayload): Promise>; @@ -197,6 +207,23 @@ export async function cloudApiFactory( } }, + async listEnvironments({ name }): Promise> { + try { + const response = await axiosCloudAPI.get(`/projects/${name}/environments`); + + if (response.status !== 200) { + throw new Error('Error fetching cloud environments from the server.'); + } + + return response; + } catch (error) { + logger.debug( + "🥲 Oops! Couldn't retrieve your project's environments from the server. Please try again." + ); + throw error; + } + }, + async getProject({ name }): Promise> { try { const response = await axiosCloudAPI.get(`/projects/${name}`); diff --git a/packages/cli/cloud/src/types.ts b/packages/cli/cloud/src/types.ts index 2e2e62c838..5e67a9225f 100644 --- a/packages/cli/cloud/src/types.ts +++ b/packages/cli/cloud/src/types.ts @@ -39,6 +39,10 @@ export type StrapiCloudCommand = (params: { ctx: CLIContext; }) => void | Command | Promise; +export type StrapiCloudNamespaceCommand = (params: { + command: Command; +}) => void | Command | Promise; + export type StrapiCloudCommandInfo = { name: string; description: string; diff --git a/packages/core/admin/admin/src/pages/Marketplace/components/NpmPackageCard.tsx b/packages/core/admin/admin/src/pages/Marketplace/components/NpmPackageCard.tsx index 5b34b73fc6..eb19e6b13a 100644 --- a/packages/core/admin/admin/src/pages/Marketplace/components/NpmPackageCard.tsx +++ b/packages/core/admin/admin/src/pages/Marketplace/components/NpmPackageCard.tsx @@ -66,7 +66,10 @@ const NpmPackageCard = ({ }`; const versionRange = semver.validRange(attributes.strapiVersion); - const isCompatible = semver.satisfies(strapiAppVersion ?? '', versionRange ?? ''); + + const isCompatible = versionRange + ? semver.satisfies(strapiAppVersion ?? '', versionRange) + : false; return (