diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee3fd2..e89e805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 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 +- Ability to run server metadata reset +- Clone command + +# Release 0.0.12 + +- Bugfixes +- General improvements + # Release 0.0.11 - Bugfix for Node LTS version, ERR_UNKNOWN_BUILTIN_MODULE @@ -13,17 +28,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/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/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/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/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/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/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/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 ed91fb2..7b29dd7 100755 --- a/src/index.js +++ b/src/index.js @@ -12,11 +12,20 @@ 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'; 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, @@ -27,6 +36,8 @@ 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 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'; @@ -93,14 +104,74 @@ 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') + .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 .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') @@ -158,6 +229,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') @@ -214,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') @@ -225,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') diff --git a/src/templates/index.js b/src/templates/index.js index 0bb71a6..7a2f468 100644 --- a/src/templates/index.js +++ b/src/templates/index.js @@ -6,12 +6,14 @@ 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/ + - .vscode/ + - .DS_Store # metadata_git_repository_sync_interval: "" # metadata_git_repository: "" @@ -25,9 +27,14 @@ 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 + +# Mac +.DS_Store `; export const configEnvTemplate = (envName) => { @@ -39,7 +46,11 @@ admin_secret: "{{ENV_${`${envName}`.toUpperCase()}_HLAMBDA_ADMIN_SECRET}}" export const packageJsonTemplate = `{ "type": "module", - "dependencies": {} + "dependencies": { + "express": "latest", + "express-async-handler": "latest", + "hlambda": "latest" + } } `; @@ -108,10 +119,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 `; 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');