Skip to content

Commit

Permalink
feat(cloud-cli): add cloud commands
Browse files Browse the repository at this point in the history
feat(cli-logger): enhance login, storing file in XDG file path
Create the user if it doesn't exist
Update logs
Use spinner for authentication
fix(cli-logger): enhance error handling and logging
fix(cli-logger): update success message
chore: update progress bar log
chore: update types and logs
feat: adding version handling and project creation questions
chore(cli): remove too restrictive types
chore(cli): remove too restrictive types
refactor(cli): move cloud project creation questions data to config in api
chore(cli): add logs for errors cases
chore(cli): remove unnecessary cast
feat(cli): update api url for cloud cli - use env var to change it
fix(cli): use existing pkg loader function
test(cli): update compress-files test
chore(cli): prettier
chore(cli): update silent command description
fix(cli): add debug and silent as options to the command
fix: update commands to use the provided command instance
chore: update debug log
fix: export
feat: add cloud commands to strapi commands
chore: various lint fixes
chore: remove unnecessary await
feat(cli): add cloud commands
  • Loading branch information
nathan-pichon committed Apr 23, 2024
1 parent bbb5d9c commit 42d0785
Show file tree
Hide file tree
Showing 43 changed files with 6,583 additions and 3,960 deletions.
4 changes: 4 additions & 0 deletions packages/cli/cloud/.eslintignore
@@ -0,0 +1,4 @@
node_modules/
.eslintrc.js
dist/
bin/
4 changes: 4 additions & 0 deletions packages/cli/cloud/.eslintrc.js
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['custom/back/typescript'],
};
22 changes: 22 additions & 0 deletions packages/cli/cloud/LICENSE
@@ -0,0 +1,22 @@
Copyright (c) 2015-present Strapi Solutions SAS

Portions of the Strapi software are licensed as follows:

* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".

* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.

MIT Expat License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions packages/cli/cloud/README.md
@@ -0,0 +1,3 @@
# Cloud CLI

This package includes the `cloud` CLI to manage Strapi projects on the cloud.
7 changes: 7 additions & 0 deletions packages/cli/cloud/bin/index.js
@@ -0,0 +1,7 @@
#!/usr/bin/env node

'use strict';

const { runStrapiCloudCommand } = require('../dist/bin');

runStrapiCloudCommand(process.argv);
77 changes: 77 additions & 0 deletions packages/cli/cloud/package.json
@@ -0,0 +1,77 @@
{
"name": "@strapi/cloud-cli",
"version": "4.23.1",
"description": "Commands to interact with the Strapi Cloud",
"keywords": [
"strapi",
"cloud",
"cli"
],
"homepage": "https://strapi.io",
"bugs": {
"url": "https://github.com/strapi/strapi/issues"
},
"repository": {
"type": "git",
"url": "git://github.com/strapi/strapi.git"
},
"license": "SEE LICENSE IN LICENSE",
"author": {
"name": "Strapi Solutions SAS",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi Solutions SAS",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
],
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"bin": "./bin/index.js",
"files": [
"./dist",
"./bin"
],
"scripts": {
"build": "pack-up build",
"clean": "run -T rimraf ./dist",
"lint": "run -T eslint .",
"watch": "pack-up watch"
},
"dependencies": {
"@strapi/utils": "4.23.1",
"axios": "1.6.0",
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"commander": "8.3.0",
"eventsource": "2.0.2",
"inquirer": "8.2.5",
"jsonwebtoken": "9.0.0",
"jwks-rsa": "3.1.0",
"lodash": "4.17.21",
"minimatch": "9.0.3",
"open": "8.4.0",
"ora": "5.4.1",
"pkg-up": "3.1.0",
"tar": "6.1.13",
"xdg-app-paths": "8.3.0",
"yup": "0.32.9"
},
"devDependencies": {
"@strapi/pack-up": "4.23.0",
"@types/cli-progress": "3.11.5",
"@types/eventsource": "1.1.15",
"@types/lodash": "^4.14.191",
"eslint-config-custom": "4.23.1",
"tsconfig": "4.23.1"
},
"engines": {
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}
14 changes: 14 additions & 0 deletions packages/cli/cloud/packup.config.ts
@@ -0,0 +1,14 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from '@strapi/pack-up';

export default defineConfig({
bundles: [
{
source: './src/index.ts',
import: './dist/index.mjs',
require: './dist/index.js',
},
],
dist: './dist',
runtime: 'node',
});
54 changes: 54 additions & 0 deletions packages/cli/cloud/src/bin.ts
@@ -0,0 +1,54 @@
import { Command } from 'commander';
import { createLogger } from './utils/logger';
import { CLIContext } from './types';
import { cli } from './index';

function buildStrapiCloudCommand(argv = process.argv, command = new Command()) {
const cloudCommands = {
loginCommand: cli.login.command,
logoutCommand: cli.logout.command,
deployCommand: cli.deployProject.command,
} as const;

// Initial program setup
command.storeOptionsAsProperties(false).allowUnknownOption(true);

// Help command
command.helpOption('-h, --help', 'Display help for command');
command.addHelpCommand('help [command]', 'Display help for command');

// Debug and silent options
command.option('-d, --debug', 'Output extra debugging');
command.option('-s, --silent', 'Output less information');

const cwd = process.cwd();

const hasDebug = argv.includes('--debug');
const hasSilent = argv.includes('--silent');

const logger = createLogger({ debug: hasDebug, silent: hasSilent, timestamp: false });

const ctx = {
cwd,
logger,
} satisfies CLIContext;

const keys = Object.keys(cloudCommands) as (keyof typeof cloudCommands)[];

// Load all commands
keys.forEach((name) => {
try {
// Add this command to the Commander command object
cloudCommands[name]({ command, argv, ctx });
} catch (e) {
console.error(`Failed to load command ${name}`, e);
}
});
}

function runStrapiCloudCommand(argv = process.argv, command = new Command()) {
buildStrapiCloudCommand(argv, command);
command.parse(argv);
}

export { buildStrapiCloudCommand, runStrapiCloudCommand };
5 changes: 5 additions & 0 deletions packages/cli/cloud/src/config/api.ts
@@ -0,0 +1,5 @@
import { env } from '@strapi/utils';

export const apiConfig = {
apiBaseUrl: env('STRAPI_CLI_CLOUD_API', 'https://cli.cloud.strapi.io'),
};
59 changes: 59 additions & 0 deletions packages/cli/cloud/src/config/local.ts
@@ -0,0 +1,59 @@
import path from 'path';
import os from 'os';
import fs from 'fs';
import XDGAppPaths from 'xdg-app-paths';

const APP_FOLDER_NAME = 'com.strapi.cli';

export const CONFIG_FILENAME = 'config.json';

export type LocalConfig = {
token?: string;
};

function checkDirectoryExists(directoryPath: string) {
try {
return fs.lstatSync(directoryPath).isDirectory();
} catch (e) {
return false;
}
}

// Determine storage path based on the operating system
export function getTmpStoragePath() {
const storagePath = path.join(os.tmpdir(), APP_FOLDER_NAME);
if (!checkDirectoryExists(storagePath)) {
fs.mkdirSync(storagePath, { recursive: true });
}
return storagePath;
}

function getConfigPath() {
const configDirs = XDGAppPaths(APP_FOLDER_NAME).configDirs();
const configPath = configDirs.find(checkDirectoryExists);

if (!configPath) {
fs.mkdirSync(configDirs[0], { recursive: true });
return configDirs[0];
}
return configPath;
}

export function getLocalConfig(): LocalConfig {
const configPath = getConfigPath();
const configFilePath = path.join(configPath, CONFIG_FILENAME);
if (!fs.existsSync(configFilePath)) {
return {};
}
try {
return JSON.parse(fs.readFileSync(configFilePath, 'utf8'));
} catch (e) {
return {};
}
}

export function saveLocalConfig(data: LocalConfig) {
const configPath = getConfigPath();
const configFilePath = path.join(configPath, CONFIG_FILENAME);
fs.writeFileSync(configFilePath, JSON.stringify(data), { encoding: 'utf8', mode: 0o600 });
}
60 changes: 60 additions & 0 deletions packages/cli/cloud/src/create-project/action.ts
@@ -0,0 +1,60 @@
import inquirer from 'inquirer';
import { AxiosError } from 'axios';
import { defaults } from 'lodash/fp';
import type { CLIContext, ProjectAnswers, ProjectInput } from '../types';
import { tokenServiceFactory, cloudApiFactory } from '../services';

function handleError(ctx: CLIContext, error: Error) {
const tokenService = tokenServiceFactory(ctx);
const { logger } = ctx;

logger.debug(JSON.stringify(error));
if (error instanceof AxiosError) {
switch (error.response?.status) {
case 401:
logger.error('Your session has expired. Please log in again.');
tokenService.eraseToken();
return;
case 403:
logger.error(
error.response.data ||
'You do not have permission to create a project. Please contact support for assistance.'
);
return;
case 400:
logger.error('Invalid input. Please check your inputs and try again.');
return;
default:
break;
}
}
logger.error(
'We encountered an issue while creating your project. Please try again in a moment. If the problem persists, contact support for assistance.'
);
}

export default (ctx: CLIContext) => {
const { getValidToken } = tokenServiceFactory(ctx);

return async () => {
const token = await getValidToken();
if (!token) {
return;
}
const cloudApi = cloudApiFactory(token);
const { data: config } = await cloudApi.config();
const { questions, defaults: defaultValues } = config.projectCreation;

const projectAnswersDefaulted = defaults(defaultValues);
const projectAnswers = await inquirer.prompt<ProjectAnswers>(questions);

const projectInput: ProjectInput = projectAnswersDefaulted(projectAnswers);

try {
const { data } = await cloudApi.createProject(projectInput);
return data;
} catch (error: Error | unknown) {
handleError(ctx, error as Error);
}
};
};
9 changes: 9 additions & 0 deletions packages/cli/cloud/src/create-project/index.ts
@@ -0,0 +1,9 @@
import action from './action';

export { action };

export default {
name: 'create-project',
description: 'Create a new project',
action,
};

0 comments on commit 42d0785

Please sign in to comment.