├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── README.md ├── bin └── 2fa.js ├── commitlint.config.js ├── images └── demo.gif ├── jest.config.js ├── package.json ├── prettier.config.js ├── release.config.js ├── src ├── command-register.ts ├── commands │ ├── add.ts │ ├── generate.ts │ ├── list.ts │ └── remove.ts ├── constants.ts ├── index.ts ├── logger.ts ├── storage.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_size = 2 11 | indent_style = space 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | bin 4 | jest.config.js 5 | examples 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['prettier', 'plugin:@typescript-eslint/recommended'], 5 | plugins: ['prettier'], 6 | env: { 7 | es6: true, 8 | jest: true, 9 | node: true, 10 | }, 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | }, 18 | globals: { 19 | on: true, // for the Socket file 20 | }, 21 | rules: { 22 | 'array-bracket-spacing': [ 23 | 'error', 24 | 'never', 25 | { 26 | objectsInArrays: false, 27 | arraysInArrays: false, 28 | }, 29 | ], 30 | 'arrow-parens': ['error', 'always'], 31 | 'arrow-spacing': ['error', { before: true, after: true }], 32 | 'comma-dangle': ['error', 'always-multiline'], 33 | curly: 'error', 34 | 'eol-last': 'error', 35 | 'func-names': 'off', 36 | 'id-length': [ 37 | 'error', 38 | { 39 | min: 1, 40 | max: 50, 41 | properties: 'never', 42 | exceptions: ['e', 'i', 'n', 't', 'x', 'y', 'z', '_', '$'], 43 | }, 44 | ], 45 | '@typescript-eslint/no-unused-vars': 'warn', 46 | '@typescript-eslint/no-explicit-any': 'off', 47 | '@typescript-eslint/explicit-module-boundary-types': 'off', 48 | '@typescript-eslint/ban-ts-comment': 'off', 49 | 'no-alert': 'error', 50 | 'no-console': 'off', 51 | 'no-const-assign': 'error', 52 | 'no-else-return': 'error', 53 | 'no-empty': 'off', 54 | 'no-undef': 'error', 55 | 'no-unused-vars': 'error', 56 | 'no-use-before-define': 'error', 57 | 'no-useless-constructor': 'error', 58 | 'object-curly-newline': 'off', 59 | 'object-shorthand': 'off', 60 | 'prefer-const': 'error', 61 | 'prefer-destructuring': ['error', { object: true, array: false }], 62 | quotes: [ 63 | 'error', 64 | 'single', 65 | { 66 | allowTemplateLiterals: true, 67 | avoidEscape: true, 68 | }, 69 | ], 70 | semi: ['error', 'always'], 71 | 'spaced-comment': 'error', 72 | strict: ['error', 'global'], 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.log 5 | 6 | v8-compile-cache-* 7 | jest 8 | coverage 9 | testProjects/*/package-lock.json 10 | testProjects/*/yarn.lock 11 | 12 | node_modules 13 | .vscode 14 | .eslintcache 15 | dist 16 | .idea 17 | build 18 | .env* 19 | env.js 20 | package-lock.json 21 | yarn.lock 22 | tsconfig.tsbuildinfo 23 | !.env.example 24 | 25 | serverless.yml 26 | slsplus.yml 27 | 28 | .slsplus 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | tests 5 | test 6 | __tests__ 7 | __test__ 8 | examples 9 | .github 10 | .env* 11 | jest.config.js 12 | .eslintrc.js 13 | .eslintignore 14 | .prettierignore 15 | prettier.config.js 16 | release.config.js 17 | commitlint.config.js 18 | .editorconfig 19 | 20 | # source code 21 | /src 22 | !/dist/src 23 | 24 | tsconfig.json 25 | slsplus.yml 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | CHANGELOG.md 4 | examples 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.5](https://github.com/yugasun/2fa/compare/v0.0.4...v0.0.5) (2024-01-31) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * catch copy to clipboard error ([b6b7d80](https://github.com/yugasun/2fa/commit/b6b7d80f11e231bc1fce6bd90e5f54bfdc13fb48)) 7 | 8 | ## [0.0.4](https://github.com/yugasun/2fa/compare/v0.0.3...v0.0.4) (2023-11-02) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * update deps ([d559c88](https://github.com/yugasun/2fa/commit/d559c8814ba2ee2fb68b2b5c405d997c030929c9)) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Two Factor Authentication 2 | 3 | [![npm](https://img.shields.io/npm/v/%40ygkit%2F2fa)](http://www.npmtrends.com/%40ygkit%2F2fa) 4 | [![NPM downloads](http://img.shields.io/npm/dm/%40ygkit%2F2fa.svg?style=flat-square)](http://www.npmtrends.com/%40ygkit%2F2fa) 5 | 6 | CLI Command for Two Factor Authentication. 7 | 8 | - [Two Factor Authentication](#two-factor-authentication) 9 | - [Demo](#demo) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Add Platform](#add-platform) 13 | - [Remove Platform](#remove-platform) 14 | - [Generate](#generate) 15 | - [License](#license) 16 | 17 | ## Demo 18 | 19 | ![Demo](images/demo.gif) 20 | 21 | ## Installation 22 | 23 | ```bash 24 | $ npm i @ygkit/2fa -g 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```bash 30 | Usage: 2fa [options] [command] 31 | 32 | Options: 33 | -v, --version output the current version 34 | -h, --help display help for command 35 | 36 | Commands: 37 | add Add platform 2fa key 38 | generate Generate 2fa code. 39 | list List all platform 40 | remove Remove platform ga key 41 | help [command] display help for command 42 | 43 | Example call: 44 | $ 2fa --help 45 | ``` 46 | 47 | ### Add Platform 48 | 49 | ```bash 50 | $ 2fa add 51 | ? Please input platform name: test1 52 | ? Please input 2fa authentication key: ********** 53 | SUCCESS Add platform test1 success. 54 | ✨ Done in 33.32s. 55 | ``` 56 | 57 | ### Remove Platform 58 | 59 | ```bash 60 | $ 2fa remove 61 | ? Please select platform name to remove: test1 62 | SUCCESS Remove platform 'test1' success. 63 | ``` 64 | 65 | ### Generate 66 | 67 | Help: 68 | 69 | ```bash 70 | $ 2fa generate 71 | ? Please select platform name: npm 72 | Generate 2fa code for npm: 73 | 928576 74 | Auto copy 2fa code to clipboard success. 75 | ``` 76 | 77 | Also you can omit `generate` word, like: 78 | 79 | ```bash 80 | $ 2fa 81 | ``` 82 | 83 | ## License 84 | 85 | MIT License 86 | 87 | Copyright (c) 2022 @yugasun. 88 | -------------------------------------------------------------------------------- /bin/2fa.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/src'); 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | const Configuration = { 2 | /* 3 | * Resolve and load @commitlint/config-conventional from node_modules. 4 | * Referenced packages must be installed 5 | */ 6 | extends: ['@commitlint/config-conventional'], 7 | }; 8 | 9 | module.exports = Configuration; 10 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yugasun/2fa/e4d78dc5c04f44228754feb1c3a894f03c976e70/images/demo.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const isDebug = process.env.DEBUG === 'true'; 2 | 3 | const config = { 4 | verbose: true, 5 | silent: !isDebug, 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest', 8 | }, 9 | testTimeout: 60000, 10 | testEnvironment: 'node', 11 | testRegex: '/__tests__/.*\\.(test|spec)\\.(js|ts)$', 12 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 14 | }; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ygkit/2fa", 3 | "version": "0.0.5", 4 | "description": "CLI Command for Two Factor Authentication", 5 | "author": "yugasun", 6 | "publishConfig": { 7 | "access": "public", 8 | "registry": "https://registry.npmjs.org/" 9 | }, 10 | "license": "MIT", 11 | "exports": "./dist/src/index.js", 12 | "bin": { 13 | "2fa": "bin/2fa.js" 14 | }, 15 | "scripts": { 16 | "dev": "ts-node ./src/index.ts", 17 | "start": "node ./bin/2fa.js", 18 | "test": "jest", 19 | "build": "tsc", 20 | "format": "npm run lint && npm run prettier", 21 | "commitlint": "commitlint -f HEAD@{15}", 22 | "lint": "eslint --ext .js,.ts,.tsx .", 23 | "lint:fix": "eslint --fix --ext .js,.ts,.tsx .", 24 | "prettier": "prettier --check '**/*.{css,html,js,ts,json,md,yaml,yml}'", 25 | "prettier:fix": "prettier --write '**/*.{css,html,js,ts,json,md,yaml,yml}'", 26 | "release": "semantic-release", 27 | "release-local": "node -r dotenv/config node_modules/semantic-release/bin/semantic-release --no-ci", 28 | "check-dependencies": "npx npm-check --skip-unused --update" 29 | }, 30 | "dependencies": { 31 | "@types/inquirer": "^8.2.9", 32 | "@types/js-yaml": "^4.0.8", 33 | "chalk": "^4.1.2", 34 | "clipboardy": "^2.3.0", 35 | "commander": "^9.5.0", 36 | "inquirer": "^8.2.6", 37 | "inquirer-search-list": "^1.2.6", 38 | "js-yaml": "^4.1.0", 39 | "otplib": "^12.0.1" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^12.1.4", 43 | "@commitlint/config-conventional": "^12.1.4", 44 | "@semantic-release/changelog": "^6.0.3", 45 | "@semantic-release/commit-analyzer": "^9.0.2", 46 | "@semantic-release/git": "^10.0.1", 47 | "@semantic-release/npm": "^9.0.2", 48 | "@semantic-release/release-notes-generator": "^10.0.3", 49 | "@types/jest": "^26.0.24", 50 | "@types/node": "^17.0.45", 51 | "@typescript-eslint/eslint-plugin": "^5.62.0", 52 | "@typescript-eslint/parser": "^5.62.0", 53 | "dotenv": "^16.3.1", 54 | "eslint": "^8.52.0", 55 | "eslint-config-prettier": "^8.10.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "husky": "^6.0.0", 58 | "jest": "^27.5.1", 59 | "lint-staged": "^11.2.6", 60 | "prettier": "^2.8.8", 61 | "semantic-release": "^19.0.5", 62 | "ts-jest": "^27.1.5", 63 | "ts-node": "^10.9.1", 64 | "typescript": "^4.9.5", 65 | "typescript-json-schema": "^0.50.1" 66 | }, 67 | "engines": { 68 | "node": ">=12.0" 69 | }, 70 | "husky": { 71 | "hooks": { 72 | "pre-commit": "lint-staged", 73 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 74 | "pre-push": "npm run lint:fix && npm run prettier:fix" 75 | } 76 | }, 77 | "lint-staged": { 78 | "**/*.{js,ts,tsx}": [ 79 | "npm run lint:fix" 80 | ], 81 | "**/*.{css,html,js,ts,json,md,yaml,yml}": [ 82 | "npm run prettier:fix" 83 | ] 84 | }, 85 | "keywords": [ 86 | "2fa", 87 | "Two Factor Authentication", 88 | "CLI" 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | printWidth: 100, 4 | semi: true, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'all', 8 | }; 9 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verifyConditions: [ 3 | '@semantic-release/changelog', 4 | '@semantic-release/npm', 5 | '@semantic-release/git', 6 | '@semantic-release/github', 7 | ], 8 | plugins: [ 9 | [ 10 | '@semantic-release/commit-analyzer', 11 | { 12 | preset: 'angular', 13 | parserOpts: { 14 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'], 15 | }, 16 | }, 17 | ], 18 | [ 19 | '@semantic-release/release-notes-generator', 20 | { 21 | preset: 'angular', 22 | parserOpts: { 23 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'], 24 | }, 25 | writerOpts: { 26 | commitsSort: ['subject', 'scope'], 27 | }, 28 | }, 29 | ], 30 | [ 31 | '@semantic-release/changelog', 32 | { 33 | changelogFile: 'CHANGELOG.md', 34 | }, 35 | ], 36 | [ 37 | '@semantic-release/npm', 38 | { 39 | pkgRoot: '.', 40 | // TODO: if don't want to auto npm publish, setup npmPublish to false 41 | npmPublish: true, 42 | tarballDir: false, 43 | }, 44 | ], 45 | [ 46 | '@semantic-release/git', 47 | { 48 | assets: ['package.json', 'src/**', 'CHANGELOG.md'], 49 | message: 'chore(release): version ${nextRelease.version} \n\n${nextRelease.notes}', 50 | }, 51 | ], 52 | [ 53 | '@semantic-release/github', 54 | { 55 | assets: ['!.env'], 56 | }, 57 | ], 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /src/command-register.ts: -------------------------------------------------------------------------------- 1 | import { program } from 'commander'; 2 | import { readdirSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { loadDynamicModule } from './utils'; 5 | 6 | export interface Options { 7 | cmdDir: string; 8 | } 9 | 10 | const defaultOptions = { 11 | cmdDir: join(process.cwd(), 'commands'), 12 | }; 13 | 14 | export class Register { 15 | command: typeof program; 16 | options: Options; 17 | commands: { name: string; path: string }[]; 18 | 19 | constructor(command: typeof program, options: Options = defaultOptions) { 20 | this.command = command || program; 21 | 22 | this.options = options; 23 | this.commands = []; 24 | } 25 | 26 | getCommands() { 27 | const { cmdDir } = this.options; 28 | const files = readdirSync(cmdDir); 29 | for (const file of files) { 30 | if (/\.(t|j)s$/g.test(file) && !/\.d\.(m)?(t|j)s$/g.test(file)) { 31 | const [commandName] = file.split('.'); 32 | const cmdPath = join(cmdDir, file); 33 | this.commands.push({ 34 | name: commandName, 35 | path: cmdPath, 36 | }); 37 | } 38 | } 39 | } 40 | 41 | async run() { 42 | this.getCommands(); 43 | 44 | for (const { path: cmdPath } of this.commands) { 45 | const cmd = await loadDynamicModule(cmdPath); 46 | this.command.addCommand(cmd); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/add.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../logger'; 2 | import { StorageEngine } from '../storage'; 3 | import { Command } from 'commander'; 4 | import { prompt } from 'inquirer'; 5 | 6 | const questions = [ 7 | { 8 | type: 'input', 9 | name: 'name', 10 | message: 'Please input platform name:', 11 | }, 12 | { 13 | type: 'password', 14 | mask: '*', 15 | name: 'key', 16 | message: 'Please input google authentication key:', 17 | validate(value: string) { 18 | if (value) { 19 | return true; 20 | } 21 | 22 | return 'Please enter a valid 2fa Key.'; 23 | }, 24 | }, 25 | ]; 26 | 27 | export async function action(): Promise { 28 | const answers = await prompt(questions); 29 | const { name, key } = answers; 30 | const store = new StorageEngine(); 31 | store.set(name, key); 32 | Logger.success(`Add platform ${name} success.`); 33 | } 34 | 35 | const cmd = new Command('add'); 36 | 37 | cmd.description('Add platform 2fa key').action(() => { 38 | action(); 39 | }); 40 | 41 | export default cmd; 42 | -------------------------------------------------------------------------------- /src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { Command } from 'commander'; 3 | import { prompt, registerPrompt } from 'inquirer'; 4 | import { authenticator } from 'otplib'; 5 | import clipboard from 'clipboardy'; 6 | import AddCommand from './add'; 7 | import { StorageEngine } from '../storage'; 8 | 9 | // eslint-disable-next-line 10 | registerPrompt('search-list', require('inquirer-search-list')); 11 | 12 | export class TwoFa { 13 | engine: StorageEngine; 14 | 15 | constructor() { 16 | this.engine = new StorageEngine(); 17 | } 18 | 19 | async gotoAdd() { 20 | const { add } = await prompt([ 21 | { 22 | type: 'confirm', 23 | name: 'add', 24 | message: 'No platform exist, do you want to add one?', 25 | }, 26 | ]); 27 | if (add) { 28 | process.argv[2] = 'add'; 29 | AddCommand.parse(process.argv); 30 | } 31 | } 32 | 33 | async generate() { 34 | const keys = this.engine.getKeys(); 35 | if (keys.length < 1) { 36 | return await this.gotoAdd(); 37 | } 38 | const { name } = await prompt([ 39 | { 40 | type: 'search-list', 41 | name: 'name', 42 | message: 'Please select platform name:', 43 | choices: keys, 44 | }, 45 | ]); 46 | const secret = this.engine.get(name) as string; 47 | 48 | const code = authenticator.generate(secret); 49 | console.log(`Generate 2fa code for plarform '${name}': `); 50 | console.log(chalk.bgGreen(` ${chalk.black(code)} `)); 51 | 52 | try { 53 | await clipboard.write(code); 54 | console.log(chalk.gray('Auto copy 2fa code to clipboard success.')); 55 | } catch (e) { 56 | console.warn(chalk.yellow('Can not copy 2fa code to clipboard, please copy it manually.')); 57 | } 58 | } 59 | } 60 | 61 | const generate = new Command('generate'); 62 | generate.description('Generate 2fa code.').action(async () => { 63 | const twoFa = new TwoFa(); 64 | twoFa.generate(); 65 | }); 66 | 67 | // sub command need export default 68 | export default generate; 69 | -------------------------------------------------------------------------------- /src/commands/list.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../logger'; 2 | import { StorageEngine } from '../storage'; 3 | import { Command } from 'commander'; 4 | 5 | export async function action(): Promise { 6 | const engine = new StorageEngine(); 7 | const keys = engine.getKeys(); 8 | if (keys.length < 1) { 9 | Logger.success(`No platform exist.`); 10 | return; 11 | } 12 | Logger.info(`All added platform:`); 13 | console.log(keys.join(', ')); 14 | } 15 | 16 | const cmd = new Command('list'); 17 | 18 | cmd.description('List all platform').action(() => { 19 | action(); 20 | }); 21 | 22 | export default cmd; 23 | -------------------------------------------------------------------------------- /src/commands/remove.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../logger'; 2 | import { StorageEngine } from '../storage'; 3 | import { Command } from 'commander'; 4 | import { prompt } from 'inquirer'; 5 | 6 | export async function action(): Promise { 7 | const engine = new StorageEngine(); 8 | const keys = engine.getKeys(); 9 | if (keys.length < 1) { 10 | Logger.success(`No platform exist.`); 11 | return; 12 | } 13 | const { name } = await prompt([ 14 | { 15 | type: 'list', 16 | name: 'name', 17 | message: 'Please select platform name to remove:', 18 | choices: keys, 19 | }, 20 | ]); 21 | engine.remove(name); 22 | Logger.success(`Remove platform '${name}' success.`); 23 | } 24 | 25 | const cmd = new Command('remove'); 26 | 27 | cmd.description('Remove platform 2fa key').action(() => { 28 | action(); 29 | }); 30 | 31 | export default cmd; 32 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { homedir } from 'os'; 3 | 4 | // sls config file path 5 | export const DEFAULT_CONFIG_PATH = join(homedir(), '.2fa.yml'); 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { program } from 'commander'; 4 | import { Register } from './command-register'; 5 | 6 | async function run() { 7 | process.env.DEBUG = process.env.CI ? 'false' : process.env.DEBUG || 'false'; 8 | 9 | const pkg = await import('../package.json'); 10 | program.storeOptionsAsProperties(false); 11 | program 12 | .name('2fa') 13 | .version( 14 | `Google Authentiction CLI Version: ${pkg.version}`, 15 | '-v, --version', 16 | 'output the current version', 17 | ); 18 | 19 | const rg = new Register(program, { 20 | cmdDir: join(__dirname, 'commands'), 21 | }); 22 | 23 | await rg.run(); 24 | 25 | program.on('--help', () => { 26 | console.log(''); 27 | console.log('Example call:'); 28 | console.log(' $ 2fa --help'); 29 | }); 30 | 31 | // set default command to generate 32 | if (!process.argv[2]) { 33 | process.argv[2] = 'generate'; 34 | } 35 | 36 | program.parse(process.argv); 37 | } 38 | 39 | run(); 40 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export class Logger { 4 | static info(msg: string) { 5 | console.log(chalk.bgWhite(chalk.black(` INFO `)), msg); 6 | } 7 | static error(msg: string) { 8 | console.log(chalk.bgRed(chalk.black(' ERROR ')), chalk.red(msg)); 9 | process.exit(1); 10 | } 11 | static success(msg: string) { 12 | console.log(chalk.bgGreen(chalk.black(' SUCCESS ')), chalk.green(msg)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import { fileExist } from './utils'; 2 | import { readFileSync, writeFileSync } from 'fs'; 3 | import YAML from 'js-yaml'; 4 | import { DEFAULT_CONFIG_PATH } from './constants'; 5 | 6 | export interface StorageOptions { 7 | storePath?: string; 8 | } 9 | 10 | export class StorageEngine { 11 | path: string; 12 | 13 | constructor(options: StorageOptions = {}) { 14 | const { storePath = DEFAULT_CONFIG_PATH } = options; 15 | this.path = storePath; 16 | } 17 | 18 | readYaml(): Record { 19 | if (!fileExist(this.path)) { 20 | return {}; 21 | } 22 | const content = readFileSync(this.path, 'utf-8'); 23 | const config = YAML.load(content) as Record; 24 | return config || {}; 25 | } 26 | 27 | writeYaml(obj: Record) { 28 | const content = YAML.dump(obj); 29 | writeFileSync(this.path, content, 'utf-8'); 30 | } 31 | 32 | get(key: string): string | undefined { 33 | const config = this.readYaml(); 34 | return config[key]; 35 | } 36 | 37 | getKeys() { 38 | const config = this.readYaml(); 39 | return Object.keys(config); 40 | } 41 | 42 | set(key: string, value: string) { 43 | const config = this.readYaml(); 44 | config[key] = value; 45 | this.writeYaml(config); 46 | } 47 | 48 | remove(key: string) { 49 | const config = this.readYaml(); 50 | delete config[key]; 51 | this.writeYaml(config); 52 | } 53 | 54 | getAll(): Record { 55 | const config = this.readYaml(); 56 | return config || {}; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync } from 'fs'; 2 | 3 | export const cwdDir = process.cwd(); 4 | 5 | export async function loadDynamicModule(p: string) { 6 | const res = await import(p); 7 | return res.default || res; 8 | } 9 | 10 | /** 11 | * Checks if a file exist 12 | * 13 | * @export 14 | * @param {string} filePath 15 | * @return {*} {boolean} 16 | */ 17 | export function fileExist(filePath: string): boolean { 18 | try { 19 | const stats = lstatSync(filePath); 20 | return stats.isFile(); 21 | } catch (e) { 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["es2019"], 5 | "target": "es2019", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": false, 14 | "strict": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": ["src/**/*", "package.json"] 18 | } 19 | --------------------------------------------------------------------------------