From 81b9f3f2e60c344ac15bb7300491141e2dc0045e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Mon, 25 Apr 2022 00:49:53 +0200 Subject: [PATCH 1/7] feat: :rocket: Add ability to set default environment Add ability to set default environment in .env via cli `hl env default `, update dependencies --- CHANGELOG.md | 21 ++++++--- package-lock.json | 63 ++++++++++++++++++++++---- package.json | 3 +- src/commands/environment/add.js | 8 ---- src/commands/environment/default.js | 68 +++++++++++++++++++++++++++++ src/index.js | 9 ++++ src/templates/index.js | 2 + src/utils/loadConfigFromYAML.js | 9 ++++ 8 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 src/commands/environment/default.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee3fd2..0eeccc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# Release 0.0.13 + +- Ability to set default environment, useful for local development when running commands. (This is not the same as the default that you can set in config.yaml, this can be env specific, thus .env should contain that info) +- Update docs +- Fix security issues via npm audit + +# Release 0.0.12 + +- Bugfixes +- General improvements + # Release 0.0.11 - Bugfix for Node LTS version, ERR_UNKNOWN_BUILTIN_MODULE @@ -13,17 +24,17 @@ # Release 0.0.9 - Environments, and env management -- Override values from yaml from .env using {{}} pattern +- Override values from YAML from .env using {{}} pattern - Snippets, used for quick installation -- ~~ Analytics~~ Replaced with feedback program (We belive in privacy by default) -- Optimize node_modules deployment, such that we can deploy app without zip-ing dependencies -- Check for new version and updates +- ~~ Analytics~~ Replaced with feedback program (We believe in privacy by default) +- Optimize node_modules deployment, such that we can deploy the app without zip-ing dependencies +- Check for new versions and updates - Shell command execution on metadata apply as post apply script # Release 0.0.8 - New initialization template -- Init command flags, force and force-remove +- Init command flags, force, and force-remove - Add new command aliases - Add changelog - Fix the issue with passing -c flag to the save diff --git a/package-lock.json b/package-lock.json index 56f8d81..06b27cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "hlambda-cli", - "version": "0.0.9", + "version": "0.0.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hlambda-cli", - "version": "0.0.9", + "version": "0.0.13", "license": "Apache License 2.0", "dependencies": { "adm-zip": "^0.5.9", "colors": "^1.4.0", "commander": "^9.0.0", "dotenv": "^16.0.0", + "edit-dotenv": "^1.0.4", "figlet": "^1.5.2", "formdata-node": "^4.3.2", "hlambda": "^0.0.4", @@ -653,11 +654,36 @@ "node": ">=12" } }, + "node_modules/edit-dotenv": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/edit-dotenv/-/edit-dotenv-1.0.4.tgz", + "integrity": "sha512-te7XBIm7Z22JYln02y1T6L0RA2UioDKSxBUTVHBxPGyb7G9aq2QXc2ZaO1maMUBSjLk61M+hjgoUchU6XdARTA==", + "dependencies": { + "eol": "~0.9.0", + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/edit-dotenv/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==" + }, "node_modules/es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -2013,9 +2039,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true, "peer": true }, @@ -3487,11 +3513,32 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" }, + "edit-dotenv": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/edit-dotenv/-/edit-dotenv-1.0.4.tgz", + "integrity": "sha512-te7XBIm7Z22JYln02y1T6L0RA2UioDKSxBUTVHBxPGyb7G9aq2QXc2ZaO1maMUBSjLk61M+hjgoUchU6XdARTA==", + "requires": { + "eol": "~0.9.0", + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + } + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==" + }, "es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -4460,9 +4507,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true, "peer": true }, diff --git a/package.json b/package.json index 07191cc..1b9af11 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hlambda-cli", "type": "module", - "version": "0.0.12", + "version": "0.0.13", "description": "CLI for hlambda server.", "main": "src/index.js", "bin": { @@ -48,6 +48,7 @@ "colors": "^1.4.0", "commander": "^9.0.0", "dotenv": "^16.0.0", + "edit-dotenv": "^1.0.4", "figlet": "^1.5.2", "formdata-node": "^4.3.2", "hlambda": "^0.0.4", diff --git a/src/commands/environment/add.js b/src/commands/environment/add.js index 5aae27a..b05ab73 100644 --- a/src/commands/environment/add.js +++ b/src/commands/environment/add.js @@ -14,14 +14,6 @@ export const addEnv = async (envName, options, program) => { const cwd = path.resolve(process.cwd()); console.log('Executing in cwd:'.green, `${cwd}`.yellow); - // Load yaml configuration - const configuration = await loadConfigFromYAML(options); - - const endpoint = configuration?.endpoint ?? 'http://localhost:8081'; - const adminSecret = options?.adminSecret ?? configuration?.admin_secret ?? ''; - - // Check if configuration has already the env name - // Create env const initEnvFilePath = path.resolve(cwd, options.config, 'environments', envName); console.log(`Trying to add new environment ${envName}:`.green, `${initEnvFilePath}`.yellow); diff --git a/src/commands/environment/default.js b/src/commands/environment/default.js new file mode 100644 index 0000000..a4f7f83 --- /dev/null +++ b/src/commands/environment/default.js @@ -0,0 +1,68 @@ +import path from 'path'; +import { writeFile, mkdir, access, readFile } from 'fs/promises'; +import editDotenv from 'edit-dotenv'; + +import { errors } from './../../errors/index.js'; + +import CLIErrorHandler from './../../utils/CLIErrorHandler.js'; +import { loadConfigFromYAML } from './../../utils/loadConfigFromYAML.js'; + +import { configEnvTemplate } from './../../templates/index.js'; + +export const defaultEnv = async (envName, options, program) => { + await (async () => { + const cwd = path.resolve(process.cwd()); + console.log('Executing in cwd:'.green, `${cwd}`.yellow); + + // Create env + const initEnvFilePath = path.resolve(cwd, '.env'); + console.log( + `Trying to update .env file and set default to environment ${envName}:`.green, + `${initEnvFilePath}`.yellow + ); + + const { force, forceRemove } = options; + const includeDemoApp = !options.clean; // Flip the clean flag. + + // We don't need to check for this because we presume that he knows what he is doing... + const folderExists = await access(initEnvFilePath) + .then((result) => { + return true; + }) + .catch((error) => { + // console.log(error); + // throw new Error(errors.ERROR_FS_READ_ERROR); + return false; + }); + + // Check if the in the current cwd there is .env file, because the .env we load from current dir. + // envName + + // Read .env value if exists + const oldValueOfDotenvFile = await readFile(`${initEnvFilePath}`, 'utf-8') + .then((data) => { + // console.log(`File read ${initEnvFilePath} successfull!`.green); + return data; + }) + .catch(() => { + console.log(`File read ${initEnvFilePath}failed`.red); + }); + + // Edit dotenv with the changes provided to us + const newValueOfDotenvFile = editDotenv(oldValueOfDotenvFile, { ENV_DEFAULT_ENVIRONMENT: `"${envName}"` }); + + // Save the new values to dotenv file to the local cwd + // console.log(newValueOfDotenvFile); + await writeFile(`${initEnvFilePath}`, newValueOfDotenvFile, 'utf-8') + .then(() => { + // console.log(`File write ${initEnvFilePath} successfull!`.green); + }) + .catch(() => { + console.log(`File write ${initEnvFilePath} failed`.red); + }); + })() + .then(() => {}) + .catch(CLIErrorHandler(program)); +}; + +export default defaultEnv; diff --git a/src/index.js b/src/index.js index ed91fb2..1eb1185 100755 --- a/src/index.js +++ b/src/index.js @@ -27,6 +27,7 @@ import { } from './commands/server.js'; import addEnv from './commands/environment/add.js'; import deleteEnv from './commands/environment/delete.js'; +import defaultEnv from './commands/environment/default.js'; import dockerSnippet from './commands/snippet/docker.js'; import dockerComposeSnippet from './commands/snippet/docker-compose.js'; import portainerInstallSnippet from './commands/snippet/portainer.js'; @@ -158,6 +159,14 @@ const envDeleteProgram = environmentsProgram .option('-c, --config ', 'Path to config.yaml file.', '') .action(deleteEnv); +const envSetDefaultEnvironment = environmentsProgram + .command('default') + .alias('def') + .argument('', 'Environment name.') + .description('Sets existing environment as default, this can also be done manually by updating .env file.') + .option('-c, --config ', 'Path to config.yaml file.', '') + .action(defaultEnv); + // --- Update sub-program --- const checkForNewVersionProgram = program .command('update') diff --git a/src/templates/index.js b/src/templates/index.js index 0bb71a6..73db516 100644 --- a/src/templates/index.js +++ b/src/templates/index.js @@ -25,6 +25,8 @@ ENV_LOCAL_HLAMBDA_ADMIN_SECRET="demo" # ENV_DEV_HLAMBDA_ENDPOINT="http://dev-server:8081" # ENV_DEV_HLAMBDA_ADMIN_SECRET="demo-dev" + +# ENV_DEFAULT_ENVIRONMENT="local" `; export const rootGitIgnoreTemplate = `.env diff --git a/src/utils/loadConfigFromYAML.js b/src/utils/loadConfigFromYAML.js index 7b1eea3..9dc7a70 100644 --- a/src/utils/loadConfigFromYAML.js +++ b/src/utils/loadConfigFromYAML.js @@ -67,6 +67,15 @@ export const loadConfigFromYAML = async (options) => { throw Error(errors.ERROR_CONFIGURATION_FILE_IS_MISSING); } + // Use default if set and if options are not set. Options have priority + if ((typeof options.env === 'undefined' || options.env === '') && process.env?.ENV_DEFAULT_ENVIRONMENT) { + console.log( + `ENV_DEFAULT_ENVIRONMENT is set, and --env flag is not passed, using it to default to env: ${process.env?.ENV_DEFAULT_ENVIRONMENT}` + ); + // eslint-disable-next-line no-param-reassign + options.env = process.env?.ENV_DEFAULT_ENVIRONMENT; + } + // Check for ENV if (!(typeof options.env === 'undefined' || options.env === '')) { const configurationEnvFilePath = path.resolve(cwd, options.config, 'environments', options.env, 'config.yaml'); From 30c3adfb07a3fe55d77f237a75d58cb3892ddb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Sat, 30 Apr 2022 23:07:55 +0200 Subject: [PATCH 2/7] feat: :zap: Enable metadata ignore and post apply script by default Improve apply metadata performance by ignoring the node_modules and .git dirs by default and also executing npm install post metadata apply New projects initialised with new CLI will not work for hlambda servers that do not support execution of remote commands --- src/templates/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/templates/index.js b/src/templates/index.js index 73db516..eca3d99 100644 --- a/src/templates/index.js +++ b/src/templates/index.js @@ -6,12 +6,12 @@ admin_secret: "{{ENV_LOCAL_HLAMBDA_ADMIN_SECRET}}" # endpoint: "http://localhost:8081" # admin_secret: demo -# metadata_post_apply_script: -# - npm install --only=production +metadata_post_apply_script: + - npm install --only=production -# metadata_apply_ignore: -# - node_modules/ -# - .git/ +metadata_apply_ignore: + - node_modules/ + - .git/ # metadata_git_repository_sync_interval: "" # metadata_git_repository: "" From a60fa0661f6a15e793796399c56c615ee1f51cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Mon, 2 May 2022 22:43:06 +0200 Subject: [PATCH 3/7] feat: :technologist: Add status command Add status command that will return current Hlambda CLI status --- src/commands/status.js | 55 ++++++++++++++++++++++++++++++++++++++++++ src/index.js | 11 +++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/commands/status.js diff --git a/src/commands/status.js b/src/commands/status.js new file mode 100644 index 0000000..416c490 --- /dev/null +++ b/src/commands/status.js @@ -0,0 +1,55 @@ +import path from 'path'; +import { readFile, writeFile } from 'fs/promises'; +import fetch from 'node-fetch'; + +import { errors } from './../errors/index.js'; + +import CLIErrorHandler from './../utils/CLIErrorHandler.js'; +import { loadConfigFromYAML } from './../utils/loadConfigFromYAML.js'; + +export const status = async (options, program) => { + await (async () => { + const cwd = path.resolve(process.cwd()); + console.log('Executing in cwd:'.green, `${cwd}`.yellow); + + // Load yaml configuration (But this time catch error because even if the status is run where config is not selected we want to have output) + const configuration = await loadConfigFromYAML(options) + .then((data) => { + return data; + }) + .catch((error) => { + try { + console.log(JSON.parse(error.message).message.red); + } catch (e) { + console.log('Unknown error while reading configuration file.'.red); + console.log(error); + } + }); + + const endpoint = options?.endpoint ?? configuration?.endpoint ?? 'http://localhost:8081'; + const adminSecret = options?.adminSecret ?? configuration?.admin_secret ?? ''; + + // Check the current environment state + // console.log(`Configuration: ${JSON.stringify(configuration, null, 2)}`); + console.log(`Environment selected: ${options.env ?? '-'}`); + console.log(`Endpoint: ${endpoint ?? '-'}`); + if (options.unsafe) { + console.log(`Admin secret: ${adminSecret}`); + } else { + console.log( + `Admin secret: ${ + adminSecret + .split('') + .map((item, index) => { + return index === 0 ? item : '*'; + }) + .join('') ?? '-' + }` + ); + } + })() + .then(() => {}) + .catch(CLIErrorHandler(program)); +}; + +export default status; diff --git a/src/index.js b/src/index.js index 1eb1185..4b3243b 100755 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import figlet from 'figlet'; // Load the sub-commands import { init } from './commands/init.js'; +import { status } from './commands/status.js'; import { config } from './commands/config.js'; import { save } from './commands/save.js'; import { requests } from './commands/requests.js'; @@ -94,6 +95,16 @@ const initProgram = program .option('-f, --force-remove', 'Clean up all the files from the directory. (!!!SUPER DANGEROUS!!!)') .action(init); +// --- Initialization sub-program --- +const statusProgram = program + .command('status') + .alias('st') + .description('Get status of the Hlambda CLI in the current working directory.') + .option('-e, --env ', 'Select environment.', '') + .option('-c, --config ', 'Path to config.yaml file.', '') + .option('-u, --unsafe', 'Do not mask admin secret, output without masking secrets.', '') + .action(status); + // --- Snippet sub-program --- // Idea is to have quick snippets in CLI const snippetProgram = program From 7f420b879c6d3fa1037654ec5031c9671853a37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Sun, 5 Jun 2022 01:56:14 +0200 Subject: [PATCH 4/7] feat: :construction: Add support for code snippets Add support for code snippet for both routers and new entrypoint files. --- CHANGELOG.md | 2 + src/commands/snippet/file-snippets.js | 48 ++++++++++++++++++ src/commands/snippet/newCodeFile.js | 73 +++++++++++++++++++++++++++ src/errors/index.js | 5 ++ src/index.js | 37 ++++++++++++++ src/templates/index.js | 12 +++-- 6 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 src/commands/snippet/file-snippets.js create mode 100644 src/commands/snippet/newCodeFile.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eeccc6..3cae932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Release 0.0.13 +- Ability to create new template files, for creating new routes by the best practices +- Add support for entry point files, that do not export default router. - Ability to set default environment, useful for local development when running commands. (This is not the same as the default that you can set in config.yaml, this can be env specific, thus .env should contain that info) - Update docs - Fix security issues via npm audit diff --git a/src/commands/snippet/file-snippets.js b/src/commands/snippet/file-snippets.js new file mode 100644 index 0000000..8666650 --- /dev/null +++ b/src/commands/snippet/file-snippets.js @@ -0,0 +1,48 @@ +export const newCodeFileJavascriptRouter = (name, path, type) => { + return `import express from "express"; +import asyncHandler from "express-async-handler"; + +import { executeWithAdminRights, getEnvValue, isEnvTrue } from "hlambda"; + +// // Define constants & errors +// import constants from "./../../constants/constants.index.js"; +// import errors from "./../../errors/errors.index.js"; + +// Create express router +const router = express.Router(); + +router.${type}( + "/${path}", + asyncHandler(async (req, res) => { + console.log( + "[${path}] ${type.toUpperCase()} | Hit!" + ); + // -------------------------------------------------------------------------------- + return res.status(200).send("OK"); + }) +); + +export default router; +`; +}; + +export const newCodeFileJavascriptEntrypoint = (name) => { + return `import { executeWithAdminRights, getEnvValue, isEnvTrue } from "hlambda"; + +// // Define constants & errors +// import constants from "./../../constants/constants.index.js"; +// import errors from "./../../errors/errors.index.js"; + +// Create entrypoint function +const entrypoint = async () => { + // Do it at startup + console.log(\`[${name}] Entrypoint started!\`); + // Do it multiple times... + const timer = setInterval(() => { + console.log(\`[${name}] Tick!\`); + }, 1000 * 10); // Every 10 sec. +}; + +export default entrypoint; +`; +}; diff --git a/src/commands/snippet/newCodeFile.js b/src/commands/snippet/newCodeFile.js new file mode 100644 index 0000000..7da58b9 --- /dev/null +++ b/src/commands/snippet/newCodeFile.js @@ -0,0 +1,73 @@ +import path from 'path'; + +import { writeFile, mkdir, access } from 'fs/promises'; + +import { errors } from './../../errors/index.js'; + +import CLIErrorHandler from './../../utils/CLIErrorHandler.js'; +import { newCodeFileJavascriptRouter, newCodeFileJavascriptEntrypoint } from './file-snippets.js'; + +export const createNewSnippetCodeFile = + (fileType = 'jsr') => + async (name, options, program) => { + await (async () => { + const cwd = path.resolve(process.cwd()); + console.log('Executing in cwd:'.green, `${cwd}`.yellow); + + const { force, path: routerPath, type } = options; + console.log(options); + + const allowedTypes = 'get|post|put|delete|all'.split('|'); + if (!allowedTypes.includes(type)) { + throw new Error(errors.ERROR_NOT_ALLOWED_METHOD_TYPE); + } + + let contentOfTheNewFile = ''; + let nameOfTheNewFile = ''; + if (fileType === 'jsr') { + contentOfTheNewFile = newCodeFileJavascriptRouter(name, routerPath, type); + nameOfTheNewFile = `router.${name}.js`; + } else if (fileType === 'jse') { + contentOfTheNewFile = newCodeFileJavascriptEntrypoint(name); + nameOfTheNewFile = `entrypoint.${name}.js`; + } else { + console.error(`Unknown file type ${fileType}`.red); + } + + console.log(contentOfTheNewFile); + + // Construct the file path to the new file + const newFilePath = path.resolve(cwd, `metadata`, /* add location in future, */ nameOfTheNewFile); + console.log(`Trying to generate new file at: ${newFilePath}`); + + // Check if file exists + const fileExists = await access(newFilePath) + .then((result) => { + return true; + }) + .catch((error) => { + // console.log(error); + // throw new Error(errors.ERROR_FS_READ_ERROR); + return false; + }); + + if (fileExists && !force) { + console.error(`File exists, we do not want to overwrite anything...`.red); + } else { + if (force) { + console.log('You choose the dangerous side of the force... we will overwrite files!'.yellow); + } + await writeFile(newFilePath, contentOfTheNewFile, 'utf-8') + .then(() => { + console.log(`File write ${newFilePath} successfull!`.green); + }) + .catch(() => { + console.log(`File write ${newFilePath} failed`.red); + }); + } + })() + .then(() => {}) + .catch(CLIErrorHandler(program)); + }; + +export default createNewSnippetCodeFile; diff --git a/src/errors/index.js b/src/errors/index.js index 44b7bca..c8d6167 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -67,6 +67,11 @@ export const errors = { exitCode: 911, }, + ERROR_NOT_ALLOWED_METHOD_TYPE: { + message: 'Not allowed method type, only supported method types are: get|post|put|delete|all', + exitCode: 9, + }, + // Special errors UNKNOWN_ERROR: { message: 'Unknown server error.', diff --git a/src/index.js b/src/index.js index 4b3243b..52ee36f 100755 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ import { import addEnv from './commands/environment/add.js'; import deleteEnv from './commands/environment/delete.js'; import defaultEnv from './commands/environment/default.js'; +import createNewSnippetCodeFile from './commands/snippet/newCodeFile.js'; import dockerSnippet from './commands/snippet/docker.js'; import dockerComposeSnippet from './commands/snippet/docker-compose.js'; import portainerInstallSnippet from './commands/snippet/portainer.js'; @@ -109,10 +110,46 @@ const statusProgram = program // Idea is to have quick snippets in CLI const snippetProgram = program .command('snippets') + .alias('sn') .alias('snip') .alias('snippet') .description('Output default or create new snippets.'); +const javascriptRouter = snippetProgram + .command('javascript-router') + .alias('jsr') + .alias('r') + .argument('', 'Snippet name. Example: `demo` will create file `router.demo.js` ') + .option('-p, --path ', 'Router path', '') + .option('-t, --type ', 'Router type', 'get') + .option('-f, --force', 'Force file creation, it will write over the existing files.') + .description('Creates new JS router template') + .action(createNewSnippetCodeFile('jsr')); + +const javascriptEntrypoint = snippetProgram + .command('javascript-entrypoint') + .alias('jse') + .alias('e') + .argument('', 'Snippet name. Example: `demo` will create file `entrypoint.demo.js` ') + .option('-f, --force', 'Force file creation, it will write over the existing files.') + .description('Creates new JS router template') + .action(createNewSnippetCodeFile('jse')); + +// Maybe support in the future. +// const typescriptRouter = snippetProgram +// .command('typescript-router') +// .alias('tsr') +// .argument('', 'Snippet name. Example: `demo` will create file `router.demo.ts` ') +// .description('Creates new TS router template') +// .action(createNewSnippetCodeFile('tsr')); + +// const typescriptEntrypoint = snippetProgram +// .command('typescript-entrypoint') +// .alias('tse') +// .argument('', 'Snippet name. Example: `demo` will create file `entry.demo.ts` ') +// .description('Creates new TS router template') +// .action(createNewSnippetCodeFile('tse')); + const dockerSnippetProgram = snippetProgram .command('docker') .alias('d') diff --git a/src/templates/index.js b/src/templates/index.js index eca3d99..49a8ef3 100644 --- a/src/templates/index.js +++ b/src/templates/index.js @@ -41,7 +41,11 @@ admin_secret: "{{ENV_${`${envName}`.toUpperCase()}_HLAMBDA_ADMIN_SECRET}}" export const packageJsonTemplate = `{ "type": "module", - "dependencies": {} + "dependencies": { + "express": "latest", + "express-async-handler": "latest", + "hlambda": "latest" + } } `; @@ -110,10 +114,10 @@ envForce: # Totally dangerous but really really useful (I really hope you are aw HASURA_GRAPHQL_ADMIN_SECRET: "IWillForceThisValue" `; -export const hlambdaREADMETemplate = `# Hlambda +export const hlambdaREADMETemplate = `# Hlambda (Hyper Lambda) -This is your folder containing all the metadata needed for the app to run in Hlambda server. +This is your folder containing all the metadata needed for the app to run in the Hlambda server. -Please read more about on https://hlambda.io +Please read more about it on https://hlambda.io `; From e3cac27ba3156dc46d2c7c88628a4628db313243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Mon, 8 Aug 2022 01:35:30 +0200 Subject: [PATCH 5/7] feat: :sparkles: Add clone command Implement clone command, with ability to clone existing hl metadata from the existing server. --- src/commands/clone.js | 89 +++++++++++++++++++++++++++++++++++++++++++ src/commands/init.js | 12 +++--- src/index.js | 15 ++++++++ 3 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/commands/clone.js diff --git a/src/commands/clone.js b/src/commands/clone.js new file mode 100644 index 0000000..cde5d7b --- /dev/null +++ b/src/commands/clone.js @@ -0,0 +1,89 @@ +import path from 'path'; +import { writeFile, mkdir, access } from 'fs/promises'; +import rimraf from 'rimraf'; + +import { errors } from './../errors/index.js'; +import init from './init.js'; +import { metadataExport } from './metadata.js'; + +import CLIErrorHandler from './../utils/CLIErrorHandler.js'; + +export const clone = async (dirName, url, options, program) => { + await (async () => { + const cwd = path.resolve(process.cwd()); + console.log('Executing in cwd:'.green, `${cwd}`.yellow); + + const cloneFilePath = path.resolve(cwd, dirName); + console.log(`Trying to clone app in:`.green, `${cloneFilePath}`.yellow); + + const { force, forceRemove } = options; + const includeDemoApp = !options.clean; // Flip the clean flag. + + const folderExists = await access(cloneFilePath) + .then((result) => { + return true; + }) + .catch((error) => { + // console.log(error); + // throw new Error(errors.ERROR_FS_READ_ERROR); + return false; + }); + if (folderExists) { + if (force) { + if (forceRemove) { + if ( + typeof cloneFilePath === 'string' && + cloneFilePath !== '' && + cloneFilePath !== '/' && + cloneFilePath !== '/*' + ) { + // Sanity check !!! + console.log(`Removing everything inside ${cloneFilePath}`.red); + rimraf.sync(`${cloneFilePath}/*`); // Please be careful... + } else { + throw new Error(errors.ERROR_DANGEROUS_SANITY_CHECK_DID_NOT_PASS); + } + } + } else { + throw new Error(errors.ERROR_FOLDER_ALREADY_EXISTS); + } + } + + // Call Init + await init(dirName, options, program, true); + + // Write configuration .env with endpoint and admin secret values + const adminSecret = options?.adminSecret ?? ''; + + // !!! Important !!! Mutate options?.config to point inside hlapp + // eslint-disable-next-line no-param-reassign + options.config = `./${dirName}/`; + + const envTemplate = `# Remove "#" to uncomment the env values. +ENV_LOCAL_HLAMBDA_ENDPOINT="${url}" +ENV_LOCAL_HLAMBDA_ADMIN_SECRET="${adminSecret}" + +# ENV_DEV_HLAMBDA_ENDPOINT="http://dev-server:8081" +# ENV_DEV_HLAMBDA_ADMIN_SECRET="demo-dev" + +# ENV_DEFAULT_ENVIRONMENT="local" +`; + + await writeFile(`./${dirName}/.env`, envTemplate, 'utf-8') + .then(() => { + // console.log(`File write ${`./${dirName}/.env`} successfull!`.green); + }) + .catch(() => { + console.log(`File write ${`./${dirName}/.env`} failed`.red); + }); + + // Call metadata export + await metadataExport(options, program); + + console.log(`Directory created.`.green); + })() + .then(() => {}) + .catch(CLIErrorHandler(program)); +}; + +export default clone; diff --git a/src/commands/init.js b/src/commands/init.js index 41b7a6d..46e5ba7 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -17,7 +17,7 @@ import { import CLIErrorHandler from './../utils/CLIErrorHandler.js'; -export const init = async (dirName, options, program) => { +export const init = async (dirName, options, program, silent = false) => { await (async () => { const cwd = path.resolve(process.cwd()); console.log('Executing in cwd:'.green, `${cwd}`.yellow); @@ -166,10 +166,12 @@ export const init = async (dirName, options, program) => { }); } - console.log( - `Directory created. Execute the following commands to continue:`.green, - `\n\n ${'cd'.green} ${dirName}\n ${'hlambda'.green} console\n` - ); + if (!silent) { + console.log( + `Directory created. Execute the following commands to continue:`.green, + `\n\n ${'cd'.green} ${dirName}\n ${'hlambda'.green} console\n` + ); + } })() .then(() => {}) .catch(CLIErrorHandler(program)); diff --git a/src/index.js b/src/index.js index 52ee36f..222c0fa 100755 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import figlet from 'figlet'; // Load the sub-commands import { init } from './commands/init.js'; +import { clone } from './commands/clone.js'; import { status } from './commands/status.js'; import { config } from './commands/config.js'; import { save } from './commands/save.js'; @@ -96,6 +97,20 @@ const initProgram = program .option('-f, --force-remove', 'Clean up all the files from the directory. (!!!SUPER DANGEROUS!!!)') .action(init); +// --- Initialization sub-program --- +const cloneProgram = program + .command('clone') + .alias('c') + .description('Clone configuration and metadata from the existing hlambda server.') + .argument('', 'Folder name.') + .argument('', 'Hlambda server location.') + .option('-s, --admin-secret ', 'Admin secret used for auth.') + .option('-e, --env ', 'Select environment.', '') + .option('-c, --clean', "Don't include demo app in initial metadata.") + .option('-f, --force', 'Force re-init, it will write over the existing files.') + .option('-f, --force-remove', 'Clean up all the files from the directory. (!!!SUPER DANGEROUS!!!)') + .action(clone); + // --- Initialization sub-program --- const statusProgram = program .command('status') From 99a6a6ee807ab39bf065d02dc37a0976a8fdcd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Wed, 10 Aug 2022 13:50:51 +0200 Subject: [PATCH 6/7] feat: :sparkles: Add .DS_Store to metadata apply ignore list, and add it to .gitignore --- src/templates/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/templates/index.js b/src/templates/index.js index 49a8ef3..7a2f468 100644 --- a/src/templates/index.js +++ b/src/templates/index.js @@ -12,6 +12,8 @@ metadata_post_apply_script: metadata_apply_ignore: - node_modules/ - .git/ + - .vscode/ + - .DS_Store # metadata_git_repository_sync_interval: "" # metadata_git_repository: "" @@ -30,6 +32,9 @@ ENV_LOCAL_HLAMBDA_ADMIN_SECRET="demo" `; export const rootGitIgnoreTemplate = `.env + +# Mac +.DS_Store `; export const configEnvTemplate = (envName) => { From 76a23f6b7462eb97a2cc617f631f104ad454e0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordan=20Neki=C4=87?= Date: Tue, 16 Aug 2022 02:39:45 +0200 Subject: [PATCH 7/7] feat: :sparkles: Add ability to run server metadata reset command --- CHANGELOG.md | 2 ++ src/commands/metadata.js | 46 ++++++++++++++++++++++++++++++++++++++++ src/index.js | 20 +++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cae932..e89e805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Ability to set default environment, useful for local development when running commands. (This is not the same as the default that you can set in config.yaml, this can be env specific, thus .env should contain that info) - Update docs - Fix security issues via npm audit +- Ability to run server metadata reset +- Clone command # Release 0.0.12 diff --git a/src/commands/metadata.js b/src/commands/metadata.js index a896cf9..d39f340 100644 --- a/src/commands/metadata.js +++ b/src/commands/metadata.js @@ -42,6 +42,52 @@ export const serverReload = async (options, program) => { .catch(CLIErrorHandler(program)); }; +export const serverResetMetadata = async (options, program) => { + await (async () => { + const cwd = path.resolve(process.cwd()); + console.log('Executing in cwd:'.green, `${cwd}`.yellow); + + // Load yaml configuration + const configuration = await loadConfigFromYAML(options); + + const endpoint = configuration?.endpoint ?? 'http://localhost:8081'; + const adminSecret = options?.adminSecret ?? configuration?.admin_secret ?? ''; + + const headers = { + 'x-hlambda-admin-secret': adminSecret, + }; + const response = await fetch(`${endpoint}/console/api/v1/metadata/reset`, { + method: 'GET', + // body: formData, + headers, + }); + + if (response.status === 200) { + console.log('Metadata reset!'.green); + } + console.log(response.status); + + if (response.status !== 200) { + throw new Error(errors.ERROR_INVALID_HLAMBDA_ADMIN_SECRET); + } + + // This is magic from commander, the real flag was --no-auto-reload but we get positive logic transformation to autoReload + if (options?.autoReload) { + const responseRestart = await fetch(`${endpoint}/console/api/v1/trigger-restart`, { + method: 'GET', + // body: formData, + headers, + }); + if (responseRestart.status === 200) { + console.log('Metadata reloaded after clearing!'.green); + } + console.log(responseRestart.status); + } + })() + .then(() => {}) + .catch(CLIErrorHandler(program)); +}; + export const serverClearMetadata = async (options, program) => { await (async () => { const cwd = path.resolve(process.cwd()); diff --git a/src/index.js b/src/index.js index 222c0fa..7b29dd7 100755 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,14 @@ import { config } from './commands/config.js'; import { save } from './commands/save.js'; import { requests } from './commands/requests.js'; import { startConsole } from './commands/console.js'; -import { serverReload, serverClearMetadata, metadataApply, metadataExport, metadataSync } from './commands/metadata.js'; +import { + serverReload, + serverResetMetadata, + serverClearMetadata, + metadataApply, + metadataExport, + metadataSync, +} from './commands/metadata.js'; import { checkForNewVersion, checkWhatIsNewInCurrentVersion } from './commands/update.js'; import { serverGetLogs, @@ -286,7 +293,7 @@ const metadata = program .command('metadata') .alias('meta') .alias('m') - .description('Apply / Export / Clear / Reload metadata, your code and configurations.'); + .description('Apply / Export / Clear / Reload / Reset metadata, your code and configurations.'); metadata .command('reload') @@ -297,6 +304,15 @@ metadata .option('-s, --admin-secret ', 'Admin secret used for auth.') .action(serverReload); +metadata + .command('reset') + .alias('res') + .description('Reset existing metadata on the server. (Warning: Similar to clear!)') + .option('-e, --env ', 'Select environment.', '') + .option('-c, --config ', 'Path to config.yaml file.', '') + .option('-s, --admin-secret ', 'Admin secret used for auth.') + .action(serverResetMetadata); + metadata .command('clear') .alias('c')