├── index.d.ts ├── index.ts ├── lib ├── decorator │ ├── index.ts │ ├── shell-command.decorator.ts │ └── shell-command.decorator.spec.ts ├── helper │ ├── index.ts │ ├── deep-clone.ts │ ├── get-function-args.ts │ ├── deep-clone.spec.ts │ └── get-function-args.spec.ts ├── type │ ├── shell-component.ts │ ├── command-decorator.options.type.ts │ ├── bootstrap-options.type.ts │ ├── command.type.ts │ ├── pattern-parameter.type.ts │ └── index.ts ├── __stubs__ │ ├── empty-test.command-component.ts │ ├── remove-property.ts │ ├── test.command-component.ts │ └── remove-property.spec.ts ├── index.ts ├── shell.module.ts ├── shell.facade.ts ├── shell.bootstraper.ts ├── pattern.parser.ts ├── value-to-param.mapper.ts ├── pattern.parser.spec.ts ├── shell.registry.ts ├── value-to-param.mapper.spec.ts └── shell.registry.spec.ts ├── code-sample.png ├── shell-example.gif ├── .prettierrc ├── .npmignore ├── index.js ├── .travis.yml ├── tsconfig.json ├── .gitignore ├── LICENSE ├── tslint.json ├── package.json └── README.md /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist' 2 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist' 2 | -------------------------------------------------------------------------------- /lib/decorator/index.ts: -------------------------------------------------------------------------------- 1 | export { ShellCommand } from './shell-command.decorator' 2 | -------------------------------------------------------------------------------- /code-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmstefanski/nestjs-shell/HEAD/code-sample.png -------------------------------------------------------------------------------- /lib/helper/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deep-clone' 2 | export * from './get-function-args' 3 | -------------------------------------------------------------------------------- /shell-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmstefanski/nestjs-shell/HEAD/shell-example.gif -------------------------------------------------------------------------------- /lib/type/shell-component.ts: -------------------------------------------------------------------------------- 1 | const marker = Symbol() 2 | 3 | export abstract class ShellComponent { 4 | private [marker]: never 5 | } 6 | -------------------------------------------------------------------------------- /lib/__stubs__/empty-test.command-component.ts: -------------------------------------------------------------------------------- 1 | import { ShellComponent } from '../type' 2 | 3 | export class EmptyTestCommandComponent extends ShellComponent {} 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './type' 2 | export * from './decorator' 3 | 4 | export { ShellModule } from './shell.module' 5 | export { ShellFacade } from './shell.facade' 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # sources 2 | lib 3 | index.ts 4 | 5 | # locks 6 | package-lock.json 7 | yarn-lock.json 8 | 9 | # configs 10 | tslint.json 11 | tsconfig.json 12 | .prettierrc -------------------------------------------------------------------------------- /lib/type/command-decorator.options.type.ts: -------------------------------------------------------------------------------- 1 | export type CommandDecoratorOptions = { 2 | name: string 3 | prefix?: string 4 | description?: string 5 | pattern?: string 6 | } 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p] 4 | } 5 | exports.__esModule = true 6 | __export(require('./dist')) 7 | -------------------------------------------------------------------------------- /lib/type/bootstrap-options.type.ts: -------------------------------------------------------------------------------- 1 | export type BootstrapOptions = { 2 | prompt?: string 3 | messages?: { notFound?: string; wrongUsage?: string } 4 | shellPrinter?: (value: any) => void 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'lts/*' 4 | script: 5 | - jest --coverage 6 | after_success: 7 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 8 | -------------------------------------------------------------------------------- /lib/shell.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common' 2 | import { ShellFacade } from './shell.facade' 3 | 4 | @Global() 5 | @Module({ providers: [ShellFacade], exports: [ShellFacade] }) 6 | export class ShellModule {} 7 | -------------------------------------------------------------------------------- /lib/type/command.type.ts: -------------------------------------------------------------------------------- 1 | export type Command = { name: string; prefix?: string; description?: string; pattern?: string; handler: InputHandler } 2 | type InputHandler = (input: string[], messages: { wrongUsage: string }, print: (value: any) => void) => Promise 3 | -------------------------------------------------------------------------------- /lib/helper/deep-clone.ts: -------------------------------------------------------------------------------- 1 | export function deepClone(value: any): any { 2 | const valueType = typeof value 3 | if (valueType === 'function' || !value) { 4 | throw new Error('Cannot deep clone a function') 5 | } 6 | 7 | return JSON.parse(JSON.stringify(value)) 8 | } 9 | -------------------------------------------------------------------------------- /lib/__stubs__/remove-property.ts: -------------------------------------------------------------------------------- 1 | export function removeProperty(object: any, property: string): any { 2 | if (!object || !property) { 3 | throw new Error('Specified object or property is falsy') 4 | } 5 | 6 | const { [property]: bye, ...otherProps } = object 7 | return otherProps 8 | } 9 | -------------------------------------------------------------------------------- /lib/type/pattern-parameter.type.ts: -------------------------------------------------------------------------------- 1 | export type SinglePatternParameter = { 2 | signatureIndex: number 3 | patternIndex: number 4 | isRequired: boolean 5 | isVarargs: boolean 6 | } 7 | 8 | export type SinglePatternParameterWithValue = SinglePatternParameter & { value: string } 9 | 10 | export type PatternParameters = { [name: string]: SinglePatternParameter } 11 | -------------------------------------------------------------------------------- /lib/__stubs__/test.command-component.ts: -------------------------------------------------------------------------------- 1 | import { ShellComponent } from '../type' 2 | 3 | export class TestCommandComponent extends ShellComponent { 4 | public async noParametersCommand(): Promise { 5 | return `hello world` 6 | } 7 | 8 | public async twoParametersCommand(arg1: string, arg2: string): Promise { 9 | return `arg1: ${arg1} | arg2: ${arg2}` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/type/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './command.type' 2 | 3 | export { BootstrapOptions } from './bootstrap-options.type' 4 | export { CommandDecoratorOptions } from './command-decorator.options.type' 5 | export { Command } from './command.type' 6 | export * from './pattern-parameter.type' 7 | export { ShellComponent } from './shell-component' 8 | 9 | export type ImmutableCommand = Pick> 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es6", 11 | "sourceMap": false, 12 | "outDir": "./dist", 13 | "rootDir": "./lib", 14 | "skipLibCheck": true 15 | }, 16 | "include": ["lib/**/*"], 17 | "exclude": ["node_modules", "**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /lib/helper/get-function-args.ts: -------------------------------------------------------------------------------- 1 | export function getFunctionArgs(func: Function): any[] { 2 | if (func && func.toString()) { 3 | const functionSignatureInString = func.toString() 4 | 5 | const openingBracetIndex = functionSignatureInString.indexOf('(') 6 | const closingBracetIndex = functionSignatureInString.indexOf(')') 7 | 8 | return functionSignatureInString 9 | .substring(openingBracetIndex + 1, closingBracetIndex) 10 | .split(',') 11 | .map((arg) => arg.trim()) 12 | .filter((arg) => arg) 13 | } 14 | 15 | throw new Error('Passed parameter is undefined or not a function') 16 | } 17 | -------------------------------------------------------------------------------- /lib/shell.facade.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { bootstrapShell } from './shell.bootstraper' 3 | import { ShellRegistry } from './shell.registry' 4 | import { BootstrapOptions, ImmutableCommand, ShellComponent } from './type' 5 | 6 | @Injectable() 7 | export class ShellFacade { 8 | public async bootstrap(options: BootstrapOptions): Promise { 9 | await bootstrapShell(options) 10 | } 11 | 12 | public registerComponents(...components: ShellComponent[]): void { 13 | components.forEach((component) => ShellRegistry.registerComponent(component)) 14 | } 15 | 16 | public getAllCommands(): ImmutableCommand[] { 17 | return ShellRegistry.getImmutableCommands() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/helper/deep-clone.spec.ts: -------------------------------------------------------------------------------- 1 | import { deepClone } from './deep-clone' 2 | 3 | describe('deepClone', () => { 4 | it('should deep clone object with nested properties', () => { 5 | const value = { somePrimitive: 'hello', someObject: { someNestedPrimitive: 1337 } } 6 | 7 | const results = deepClone(value) 8 | 9 | expect(value).not.toBe(results) 10 | }) 11 | 12 | it('should throw an error when passed value has type of function', () => { 13 | const value = () => '123' 14 | expect(() => deepClone(value)).toThrowError('Cannot deep clone a function') 15 | }) 16 | 17 | it('should thorw an error when passed falsy value', () => { 18 | expect(() => deepClone(null)).toThrowError('Cannot deep clone a function') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/__stubs__/remove-property.spec.ts: -------------------------------------------------------------------------------- 1 | import { removeProperty } from './remove-property' 2 | 3 | describe('removeProperty', () => { 4 | it('should remove property from object', () => { 5 | const someObject = { a: 1, b: 2 } 6 | 7 | const results = removeProperty(someObject, 'b') 8 | 9 | expect(results).toStrictEqual({ a: 1 }) 10 | }) 11 | 12 | it('should throw an error when passed object is falsy', () => { 13 | expect(() => removeProperty(null, 'a')).toThrowError('Specified object or property is falsy') 14 | }) 15 | 16 | it('should throw an error when passed property (to remove) is falsy', () => { 17 | const someObject = { a: 1, b: 2 } 18 | expect(() => removeProperty(someObject, null)).toThrowError('Specified object or property is falsy') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/helper/get-function-args.spec.ts: -------------------------------------------------------------------------------- 1 | import { getFunctionArgs } from './get-function-args' 2 | 3 | describe('getFunctionArgs', () => { 4 | it('should return function parameters', () => { 5 | const someFunction = (abc: string, def: number) => abc + def 6 | 7 | const results = getFunctionArgs(someFunction) 8 | 9 | expect(results).toStrictEqual(['abc', 'def']) 10 | }) 11 | 12 | it(`should return function parameters from object's method`, () => { 13 | const someObjectWithMethod = { someFunction(abc: string, def: number): void {} } 14 | 15 | const results = getFunctionArgs(someObjectWithMethod.someFunction) 16 | 17 | expect(results).toStrictEqual(['abc', 'def']) 18 | }) 19 | 20 | it('should return empty array when passed function with no parameters', () => { 21 | const someFunction = () => '' 22 | 23 | const results = getFunctionArgs(someFunction) 24 | 25 | expect(results).toStrictEqual([]) 26 | }) 27 | 28 | it('should throw an error when passed falsy value', () => { 29 | expect(() => getFunctionArgs(null)).toThrowError('Passed parameter is undefined or not a function') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bartłomiej Stefański 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/shell.bootstraper.ts: -------------------------------------------------------------------------------- 1 | import { ShellRegistry } from './shell.registry' 2 | import { BootstrapOptions } from './type/bootstrap-options.type' 3 | 4 | import { createInterface } from 'readline' 5 | 6 | export async function bootstrapShell(options: BootstrapOptions = { prompt: '⤳' }): Promise { 7 | const rl = createInterface({ 8 | input: process.stdin, 9 | output: process.stdout, 10 | prompt: `${options.prompt} `, 11 | }) 12 | 13 | const print = options.shellPrinter || ((value) => console.log(value)) 14 | const messages = { 15 | notFound: `Say what? I might have heard $input`, 16 | wrongUsage: `Wrong usage: $command $pattern`, 17 | ...options.messages, 18 | } 19 | 20 | const onLine = async (line) => { 21 | if (!line) return 22 | 23 | const splittedLineResult = line.trim().split(' ') 24 | const command = ShellRegistry.findCommand(splittedLineResult[0]) 25 | 26 | if (!command) { 27 | print(messages.notFound.replace('$input', line)) 28 | return 29 | } 30 | 31 | const commandArguments = splittedLineResult.slice(1) 32 | return command.handler(commandArguments, { wrongUsage: messages.wrongUsage }, print) 33 | } 34 | 35 | rl.on('line', async (input) => onLine(input).then(() => rl.prompt())).on('close', () => process.exit()) 36 | 37 | rl.emit('line') 38 | } 39 | -------------------------------------------------------------------------------- /lib/pattern.parser.ts: -------------------------------------------------------------------------------- 1 | import { PatternParameters, SinglePatternParameter } from './type/pattern-parameter.type' 2 | 3 | export function parsePattern(pattern: string): PatternParameters { 4 | const splittedBySpace = pattern.split(' ') 5 | let results = {} 6 | 7 | if (!pattern || _hasAnyInvalidParam(splittedBySpace)) { 8 | return results 9 | } 10 | 11 | splittedBySpace.forEach((rawParam, index) => { 12 | results = { ...results, ..._mapParamToProperFormat(rawParam, index) } 13 | }) 14 | 15 | return results 16 | } 17 | 18 | function _hasAnyInvalidParam(params: string[]): boolean { 19 | const matchInvalidCharacters = /[^A-Za-z0-9_\[\]\<\>\@]+/ 20 | return params.some((param) => matchInvalidCharacters.test(param)) 21 | } 22 | 23 | function _mapParamToProperFormat(param: string, patternIndex: number): PatternParameters { 24 | const argWithoutBrackets = _removeAllBrackets(param) 25 | const paramName = _clearParameterName(argWithoutBrackets) 26 | const isRequired = param.includes('<') 27 | const isVarargs = argWithoutBrackets.includes('@') 28 | 29 | return { [paramName]: { patternIndex, isRequired, isVarargs, signatureIndex: 0 } } 30 | } 31 | 32 | function _removeAllBrackets(value: string): string { 33 | return value.replace(/\<|\>|\[|\]/g, '') 34 | } 35 | 36 | function _clearParameterName(param: string): string { 37 | return param.replace('@', '') 38 | } 39 | -------------------------------------------------------------------------------- /lib/value-to-param.mapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PatternParameters, 3 | SinglePatternParameter, 4 | SinglePatternParameterWithValue, 5 | } from './type/pattern-parameter.type' 6 | 7 | export function mapActualValueToParams( 8 | patternParams: PatternParameters, 9 | actualParamsValues: string[], 10 | ): SinglePatternParameterWithValue[] { 11 | const mutablePatternParams = patternParams || [] 12 | 13 | const arrayMappedParams = Object.values(mutablePatternParams) as SinglePatternParameter[] 14 | return arrayMappedParams 15 | .sort(_ascendingBySignatureIndex) 16 | .map((arg: SinglePatternParameter) => ({ ...arg, value: _getParamValue(arg, actualParamsValues) })) 17 | } 18 | 19 | function _getParamValue(arg: SinglePatternParameter, actualParamsValues: string[]): any { 20 | return arg.isVarargs ? _joinVarargsIfPresent(actualParamsValues, arg) : _getNormalParam(actualParamsValues, arg) 21 | } 22 | 23 | function _joinVarargsIfPresent(actualValue: string[], arg: SinglePatternParameter): string | null { 24 | const chunkedVarargs = actualValue.slice(arg.patternIndex) 25 | return chunkedVarargs.length === 0 ? null : chunkedVarargs.join(' ') 26 | } 27 | 28 | function _getNormalParam(actualValue: string[], arg: SinglePatternParameter): string | null { 29 | return actualValue[arg.patternIndex] || null 30 | } 31 | 32 | function _ascendingBySignatureIndex(a: SinglePatternParameter, b: SinglePatternParameter): number { 33 | return a.signatureIndex - b.signatureIndex 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:latest", "tslint-config-airbnb", "tslint-config-prettier"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rulesDirectory": [], 8 | "rules": { 9 | "function-name": false, 10 | "file-name-casing": [true, "kebab-case"], 11 | "no-var-requires": false, 12 | "semicolon": false, 13 | "eofline": false, 14 | "quotemark": [true, "single"], 15 | "trailing-comma": false, 16 | "member-access": true, 17 | "indent": false, 18 | "ordered-imports": [true], 19 | "no-implicit-dependencies": false, 20 | "no-unnecessary-initializer": false, 21 | "unified-signatures": false, 22 | "callable-types": false, 23 | "max-line-length": [true, 120], 24 | "member-ordering": [false], 25 | "curly": false, 26 | "interface-name": [false], 27 | "array-type": [false], 28 | "no-empty-interface": false, 29 | "no-empty": false, 30 | "arrow-parens": false, 31 | "object-literal-sort-keys": false, 32 | "no-unused-expression": false, 33 | "max-classes-per-file": [false], 34 | "ban-types": false, 35 | "variable-name": [false], 36 | "one-line": [false], 37 | "one-variable-per-declaration": [false], 38 | "no-return-await": true, 39 | "match-default-export-name": true, 40 | "prefer-readonly": true, 41 | "forin": false, 42 | "typedef": [true, "call-signature", "property-declaration"], 43 | "no-submodule-imports": false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/pattern.parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { parsePattern } from './pattern.parser' 2 | 3 | describe('parsePattern', () => { 4 | it('should parse pattern which includes two required params', () => { 5 | const pattern = ' ' 6 | 7 | const results = parsePattern(pattern) 8 | 9 | expect(results).toStrictEqual({ 10 | firstParam: { signatureIndex: 0, patternIndex: 0, isRequired: true, isVarargs: false }, 11 | secondParam: { signatureIndex: 0, patternIndex: 1, isRequired: true, isVarargs: false }, 12 | }) 13 | }) 14 | 15 | it('should parse pattern which includes one optional and one required param', () => { 16 | const pattern = ' [secondParam]' 17 | 18 | const results = parsePattern(pattern) 19 | 20 | expect(results).toStrictEqual({ 21 | firstParam: { signatureIndex: 0, patternIndex: 0, isRequired: true, isVarargs: false }, 22 | secondParam: { signatureIndex: 0, patternIndex: 1, isRequired: false, isVarargs: false }, 23 | }) 24 | }) 25 | 26 | it('should return empty object when passed falsy value (empty string)', () => { 27 | const results = parsePattern('') 28 | expect(results).toStrictEqual({}) 29 | }) 30 | 31 | it('should return empty object when passed pattern is invalid', () => { 32 | const pattern = ' [normal]' 33 | 34 | const results = parsePattern(pattern) 35 | 36 | expect(results).toStrictEqual({}) 37 | }) 38 | 39 | it('should parse pattern with name made up of only letters and numbers and underscores', () => { 40 | const pattern = '<@ABCDEFGHIJKLMN_OPQRSTUVWXYZ> [@abcdefghijklmn_opqrstuvwxyz] <_0123456789_>' 41 | 42 | const results = parsePattern(pattern) 43 | 44 | expect(results).toStrictEqual({ 45 | ABCDEFGHIJKLMN_OPQRSTUVWXYZ: { signatureIndex: 0, patternIndex: 0, isRequired: true, isVarargs: true }, 46 | abcdefghijklmn_opqrstuvwxyz: { signatureIndex: 0, patternIndex: 1, isRequired: false, isVarargs: true }, 47 | _0123456789_: { signatureIndex: 0, patternIndex: 2, isRequired: true, isVarargs: false }, 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-shell", 3 | "version": "1.4.1", 4 | "description": "An interactive shell for NestJS which allows you to plug-in your custom commands and use them when the app's running", 5 | "keywords": [ 6 | "nest", 7 | "nestjs", 8 | "shell", 9 | "console" 10 | ], 11 | "author": { 12 | "name": "Bart Stefanski", 13 | "email": "contact@bstefanski.com", 14 | "url": "https://bstefanski.com/" 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "prebuild": "rimraf dist", 19 | "build": "tsc -p tsconfig.json", 20 | "format": "prettier --write \"lib/**/*.ts\" \"lib/**/*.spec.ts\"", 21 | "prepublish": "npm run format", 22 | "publish": "", 23 | "test": "jest", 24 | "test:cov": "jest --coverage" 25 | }, 26 | "peerDependencies": { 27 | "@nestjs/common": "^8.0.4", 28 | "@nestjs/core": "^9.0.5", 29 | "reflect-metadata": "^0.1.13", 30 | "rimraf": "^3.0.2", 31 | "rxjs": "^7.2.0" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^8.0.2", 35 | "@nestjs/common": "^8.0.4", 36 | "@nestjs/core": "^9.0.5", 37 | "@nestjs/schematics": "^8.0.2", 38 | "@nestjs/testing": "^8.0.4", 39 | "@types/express": "^4.17.13", 40 | "@types/jest": "^26.0.24", 41 | "@types/node": "^16.4.3", 42 | "@types/supertest": "^2.0.11", 43 | "coveralls": "^3.1.0", 44 | "jest": "^27.0.6", 45 | "prettier": "^2.0.5", 46 | "reflect-metadata": "^0.1.13", 47 | "rimraf": "^3.0.2", 48 | "rxjs": "^6.6.3", 49 | "supertest": "^4.0.2", 50 | "ts-jest": "^27.0.4", 51 | "ts-loader": "^9.2.4", 52 | "ts-node": "^10.1.0", 53 | "tsconfig-paths": "^3.9.0", 54 | "tslint": "^6.1.2", 55 | "tslint-config-airbnb": "^5.11.2", 56 | "tslint-config-prettier": "^1.18.0", 57 | "typescript": "^3.6.3" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "lib", 66 | "testRegex": ".spec.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node", 72 | "coveragePathIgnorePatterns": [ 73 | "lib/__stubs__" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/shell.registry.ts: -------------------------------------------------------------------------------- 1 | import { deepClone } from './helper' 2 | import { Command, ImmutableCommand, ShellComponent } from './type' 3 | 4 | type Components = { [name: string]: ShellComponent } 5 | type Commands = { [nameWithPrefix: string]: Command } 6 | 7 | export class ShellRegistry { 8 | private static components: Components = {} 9 | private static commands: Commands = {} 10 | 11 | private constructor() {} 12 | 13 | public static registerComponent(component: ShellComponent): void { 14 | this.ensureComponentIsTruthy(component) 15 | const componentClassName = component.constructor.name 16 | this.components = { ...this.components, [componentClassName]: component } 17 | } 18 | 19 | private static ensureComponentIsTruthy(component: ShellComponent): void { 20 | if (!component) { 21 | throw new Error(`Component you're trying to register has falsy value (probably null or undefined)`) 22 | } 23 | } 24 | 25 | public static getComponent(componentClassName: string): ShellComponent { 26 | return this.components[componentClassName] 27 | } 28 | 29 | public static registerCommand(command: Command): void { 30 | this.ensureCommandIsTruthy(command) 31 | const prefixedCommand = command.prefix + command.name 32 | this.commands = { ...this.commands, [prefixedCommand]: command } 33 | } 34 | 35 | private static ensureCommandIsTruthy(command: Command): void { 36 | if (!command || (command && !command.name)) { 37 | throw new Error( 38 | `Cannot register command because its value or name is falsy (it may be empty command name in @ShellCommand decorator)`, 39 | ) 40 | } 41 | } 42 | 43 | public static findCommand(commandWithPrefix: string): Command { 44 | return this.commands[commandWithPrefix] 45 | } 46 | 47 | public static getImmutableComponents(): string[] { 48 | return Object.keys(deepClone(this.components)) 49 | } 50 | 51 | public static getImmutableCommands(): ImmutableCommand[] { 52 | return this.convertCommandsToArray(deepClone(this.commands)) 53 | } 54 | 55 | private static convertCommandsToArray(commandsInObject: Commands): ImmutableCommand[] { 56 | return Object.entries(commandsInObject).map((entry) => ({ ...entry[1], name: entry[0] })) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/value-to-param.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { mapActualValueToParams } from './value-to-param.mapper' 2 | 3 | describe('mapActualValueToParams', () => { 4 | it('should return sorted (ascending) pattern params with value property', () => { 5 | const patternParams = { testParam: { signatureIndex: 0, patternIndex: 0, isRequired: false, isVarargs: false } } 6 | const actualParamsValues = ['I am a value of testParam'] 7 | 8 | const results = mapActualValueToParams(patternParams, actualParamsValues) 9 | 10 | expect(results).toStrictEqual([{ ...patternParams.testParam, value: 'I am a value of testParam' }]) 11 | }) 12 | 13 | it('should return empty array when pattern params are falsy', () => { 14 | const results = mapActualValueToParams(null, ['Some value']) 15 | expect(results).toStrictEqual([]) 16 | }) 17 | 18 | it('shuld return joined (bonded) value for varargs pattern params', () => { 19 | const patternParams = { testParam: { signatureIndex: 0, patternIndex: 1, isRequired: false, isVarargs: true } } 20 | const actualParamsValues = ['thatParamShouldNotBeCounted', 'hello', 'there', 'my', 'little', 'friend'] 21 | 22 | const results = mapActualValueToParams(patternParams, actualParamsValues) 23 | 24 | expect(results).toStrictEqual([{ ...patternParams.testParam, value: 'hello there my little friend' }]) 25 | }) 26 | 27 | it('should return null when normal pattern param does not have value', () => { 28 | const patternParams = { testParam: { signatureIndex: 0, patternIndex: 0, isRequired: false, isVarargs: false } } 29 | 30 | const results = mapActualValueToParams(patternParams, []) 31 | 32 | expect(results).toStrictEqual([{ ...patternParams.testParam, value: null }]) 33 | }) 34 | 35 | it('should return null when varargs pattern param does not have value', () => { 36 | const patternParams = { testParam: { signatureIndex: 0, patternIndex: 1, isRequired: false, isVarargs: true } } 37 | 38 | const results = mapActualValueToParams(patternParams, []) 39 | 40 | expect(results).toStrictEqual([{ ...patternParams.testParam, value: null }]) 41 | }) 42 | 43 | it(`should return empty array when both passed function's arguments are falsy`, () => { 44 | const results = mapActualValueToParams(null, null) 45 | expect(results).toStrictEqual([]) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /lib/decorator/shell-command.decorator.ts: -------------------------------------------------------------------------------- 1 | import { deepClone, getFunctionArgs } from '../helper' 2 | import { parsePattern } from '../pattern.parser' 3 | import { ShellRegistry } from '../shell.registry' 4 | import { CommandDecoratorOptions } from '../type' 5 | import { PatternParameters, SinglePatternParameterWithValue } from '../type/pattern-parameter.type' 6 | import { mapActualValueToParams } from '../value-to-param.mapper' 7 | 8 | export function ShellCommand(options: CommandDecoratorOptions): MethodDecorator { 9 | return (target: object, methodName: string | symbol, descriptor: TypedPropertyDescriptor) => { 10 | const { name, prefix = '', description = '', pattern = '' } = options 11 | 12 | const handler = async (input, messages, print) => { 13 | const componentInstance = ShellRegistry.getComponent(target.constructor.name) 14 | const commandMethod = componentInstance[methodName] 15 | const patternParams: SinglePatternParameterWithValue[] = _getParsedPatternParams(pattern, commandMethod, input) 16 | 17 | if (_hasAnyRequiredParam(patternParams)) { 18 | const commandName = (prefix || '') + name 19 | print(messages.wrongUsage.replace('$command', commandName).replace('$pattern', pattern)) 20 | return 21 | } 22 | 23 | return commandMethod 24 | .apply(componentInstance, _mapParamsToValueOnly(patternParams)) 25 | .then((result) => print(result)) 26 | } 27 | 28 | ShellRegistry.registerCommand({ name, prefix, description, pattern, handler }) 29 | } 30 | } 31 | 32 | function _getParsedPatternParams( 33 | pattern: string, 34 | commandMethod: Function, 35 | input: string[], 36 | ): SinglePatternParameterWithValue[] { 37 | let patternParams: PatternParameters | SinglePatternParameterWithValue[] = parsePattern(pattern) 38 | patternParams = _replaceSignatureIndex(patternParams, getFunctionArgs(commandMethod)) 39 | patternParams = mapActualValueToParams(patternParams, input) 40 | return patternParams 41 | } 42 | 43 | function _replaceSignatureIndex(patternParams, functionArguments): PatternParameters { 44 | let mutablePatternParams: PatternParameters = deepClone(patternParams) 45 | _ensureAnyPatternParamCanBeBinded(patternParams, functionArguments) 46 | 47 | functionArguments.forEach((arg, index) => { 48 | mutablePatternParams = { ...mutablePatternParams, [arg]: { ...mutablePatternParams[arg], signatureIndex: index } } 49 | }) 50 | return mutablePatternParams 51 | } 52 | 53 | function _ensureAnyPatternParamCanBeBinded(patternParams, functionArguments): void { 54 | const patternParamsCount = Object.values(patternParams).length 55 | if (patternParamsCount > 0 && functionArguments < patternParamsCount) { 56 | const lastParamName = Object.keys(patternParams).slice(-1) 57 | throw Error(`Parameter specified in pattern has no equaivalent in actual parameters [Param name: ${lastParamName}]`) 58 | } 59 | } 60 | 61 | function _hasAnyRequiredParam(patternParams: SinglePatternParameterWithValue[]): boolean { 62 | const requiredParamsWithNoValue = patternParams.filter((param) => param.isRequired && !param.value) 63 | return requiredParamsWithNoValue.length > 0 64 | } 65 | 66 | function _mapParamsToValueOnly(patternParams: SinglePatternParameterWithValue[]): string[] { 67 | return patternParams.map((param) => param.value) 68 | } 69 | -------------------------------------------------------------------------------- /lib/shell.registry.spec.ts: -------------------------------------------------------------------------------- 1 | import { ShellRegistry } from './shell.registry' 2 | import { EmptyTestCommandComponent } from './__stubs__/empty-test.command-component' 3 | import { removeProperty } from './__stubs__/remove-property' 4 | 5 | describe('ShellRegistry', () => { 6 | /* tslint:disable no-string-literal */ 7 | beforeEach(() => { 8 | ShellRegistry['commands'] = {} 9 | ShellRegistry['components'] = {} 10 | }) 11 | 12 | describe('registerComponent', () => { 13 | it('should be pushed to components container', () => { 14 | const component = new EmptyTestCommandComponent() 15 | 16 | ShellRegistry.registerComponent(component) 17 | const results = ShellRegistry.getImmutableComponents() 18 | 19 | expect(results).toStrictEqual(['EmptyTestCommandComponent']) 20 | }) 21 | 22 | it('should throw an error if passed falsy argument as a component', () => { 23 | expect(() => ShellRegistry.registerComponent(null)).toThrowError( 24 | `Component you're trying to register has falsy value (probably null or undefined)`, 25 | ) 26 | }) 27 | }) 28 | 29 | describe('getComponent', () => { 30 | it('should return component object with same name as specified', () => { 31 | const component = new EmptyTestCommandComponent() 32 | 33 | ShellRegistry.registerComponent(component) 34 | const results = ShellRegistry.getComponent('EmptyTestCommandComponent') 35 | 36 | expect(results).toStrictEqual(new EmptyTestCommandComponent()) 37 | }) 38 | 39 | it('should throw an error if passed falsy argument as a componentClassName', () => { 40 | const component = new EmptyTestCommandComponent() 41 | 42 | ShellRegistry.registerComponent(component) 43 | const results = ShellRegistry.getComponent('') 44 | 45 | expect(results).toBeFalsy() 46 | }) 47 | 48 | it('should return undefined if passed class name does not match any component', () => { 49 | const results = ShellRegistry.getComponent('SomeCommandComponent') 50 | expect(results).toBeUndefined() 51 | }) 52 | }) 53 | 54 | describe('registerCommand', () => { 55 | it('should be pushed to commands container', () => { 56 | const command = { name: 'test', prefix: '.', handler: async (input) => {} } 57 | 58 | ShellRegistry.registerCommand(command) 59 | const results = ShellRegistry.getImmutableCommands() 60 | 61 | expect(results).toStrictEqual([{ ...removeProperty(command, 'handler'), name: command.prefix + command.name }]) 62 | }) 63 | 64 | it('should throw an error if passed falsy argument as a command', () => { 65 | expect(() => ShellRegistry.registerCommand(null)).toThrowError( 66 | `Cannot register command because its value or name is falsy (it may be empty command name in @ShellCommand decorator)`, 67 | ) 68 | }) 69 | 70 | it('should throw an error if command name are falsy', () => { 71 | const command = { name: '', prefix: '', handler: async (input) => {} } 72 | expect(() => ShellRegistry.registerCommand(command)).toThrowError( 73 | `Cannot register command because its value or name is falsy (it may be empty command name in @ShellCommand decorator)`, 74 | ) 75 | }) 76 | }) 77 | 78 | describe('findCommand', () => { 79 | it('should return command with same prefix and name as specified', () => { 80 | const command = { name: 'test', prefix: '.', handler: async (input) => {} } 81 | 82 | ShellRegistry.registerCommand(command) 83 | const results = ShellRegistry.findCommand('.test') 84 | 85 | expect(results).toStrictEqual(command) 86 | }) 87 | 88 | it('should return undefined if there is no command with such prefix and name', () => { 89 | const results = ShellRegistry.findCommand('.test') 90 | expect(results).toBeUndefined() 91 | }) 92 | }) 93 | 94 | describe('getImmutableComponents', () => { 95 | it('should return deep copy of components container', () => { 96 | const container: any = ShellRegistry.getImmutableComponents() 97 | 98 | container['hello-test'] = 'hello' 99 | 100 | expect(ShellRegistry.getImmutableComponents()).not.toStrictEqual({ ['hello-test']: 'hello' }) 101 | }) 102 | }) 103 | 104 | describe('getImmutableCommands', () => { 105 | it('should return deep copy of commands container', () => { 106 | const container: any = ShellRegistry.getImmutableCommands() 107 | 108 | container['hello-test'] = 'hello' 109 | 110 | expect(ShellRegistry.getImmutableCommands()).not.toStrictEqual({ ['hello-test']: 'hello' }) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /lib/decorator/shell-command.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { ShellRegistry } from '../shell.registry' 2 | import { CommandDecoratorOptions, ShellComponent } from '../type' 3 | import { TestCommandComponent } from '../__stubs__/test.command-component' 4 | import { ShellCommand } from './shell-command.decorator' 5 | 6 | const executeCommand = async ( 7 | name: string, 8 | args?: string[], 9 | printer: (value: any) => any = () => {}, 10 | wrongUsageMessage?: string, 11 | ) => { 12 | await ShellRegistry.findCommand(name).handler(args, { wrongUsage: wrongUsageMessage }, printer) 13 | } 14 | 15 | const registerComponentAndDecorateCommand = ( 16 | component: ShellComponent, 17 | methodName: string, 18 | decoratorOptions: CommandDecoratorOptions, 19 | ) => { 20 | ShellRegistry.registerComponent(component) 21 | ShellCommand(decoratorOptions)(component, methodName, null) 22 | } 23 | 24 | describe('ShellCommand', () => { 25 | it('should register the command', () => { 26 | const componentInstance = new TestCommandComponent() 27 | 28 | ShellCommand({ name: 'test' })(componentInstance, 'noParametersCommand', null) 29 | 30 | expect(ShellRegistry.getImmutableCommands()).toStrictEqual([ 31 | { name: 'test', pattern: '', prefix: '', description: '' }, 32 | ]) 33 | }) 34 | 35 | it(`should execute component's method bonded to the command`, async () => { 36 | const componentInstance = new TestCommandComponent() 37 | const commandMethodSpy = jest.spyOn(componentInstance, 'twoParametersCommand') 38 | 39 | registerComponentAndDecorateCommand(componentInstance, 'twoParametersCommand', { name: 'test' }) 40 | await executeCommand('test') 41 | 42 | expect(commandMethodSpy).toBeCalled() 43 | }) 44 | 45 | it('should return concatenated values of two required pattern parameters', async () => { 46 | const componentInstance = new TestCommandComponent() 47 | let results = '' 48 | 49 | registerComponentAndDecorateCommand(componentInstance, 'twoParametersCommand', { 50 | name: 'test', 51 | pattern: ' ', 52 | }) 53 | await executeCommand('test', ['first', 'second'], (v) => (results = v)) 54 | 55 | expect(results).toStrictEqual(`arg1: first | arg2: second`) 56 | }) 57 | 58 | it('should bind oposite placed pattern parameters to proper arguments', async () => { 59 | const componentInstance = new TestCommandComponent() 60 | let results = '' 61 | 62 | registerComponentAndDecorateCommand(componentInstance, 'twoParametersCommand', { 63 | name: 'test', 64 | pattern: ' <@arg1>', 65 | }) 66 | await executeCommand('test', ['first_arg', 'second', 'is', 'varargs'], (v) => (results = v)) 67 | 68 | expect(results).toStrictEqual(`arg1: second is varargs | arg2: first_arg`) 69 | }) 70 | 71 | it('should print wrong usage message when required param has no value', async () => { 72 | const componentInstance = new TestCommandComponent() 73 | let results = '' 74 | 75 | registerComponentAndDecorateCommand(componentInstance, 'twoParametersCommand', { 76 | name: 'test', 77 | pattern: '', 78 | }) 79 | await executeCommand('test', [], (v) => (results = v), 'Wrong usage: $command $pattern') 80 | 81 | expect(results).toStrictEqual('Wrong usage: test ') 82 | }) 83 | 84 | it('should throw an error when pattern param has no equivalent in actual (function) params', async () => { 85 | const componentInstance = new TestCommandComponent() 86 | 87 | registerComponentAndDecorateCommand(componentInstance, 'noParametersCommand', { 88 | name: 'test', 89 | pattern: ' [someCoolNextParam]', 90 | }) 91 | const executeCommandPromise = executeCommand('test', ['hello']) 92 | 93 | await expect(executeCommandPromise).rejects.toThrowError( 94 | 'Parameter specified in pattern has no equaivalent in actual parameters [Param name: someCoolNextParam]', 95 | ) 96 | }) 97 | 98 | it('should execute without specifying optional pattern param', async () => { 99 | const componentInstance = new TestCommandComponent() 100 | let results = '' 101 | 102 | registerComponentAndDecorateCommand(componentInstance, 'twoParametersCommand', { 103 | name: 'test', 104 | pattern: '[someOptionalParam]', 105 | }) 106 | await executeCommand('test', [], (v) => (results = v)) 107 | 108 | expect(results).toStrictEqual('arg1: null | arg2: null') 109 | }) 110 | 111 | it('should respect required parameter even when it is not first', async () => { 112 | const componentInstance = new TestCommandComponent() 113 | let results = '' 114 | 115 | registerComponentAndDecorateCommand(componentInstance, 'twoParametersCommand', { 116 | name: 'test', 117 | pattern: ' ', 118 | }) 119 | await executeCommand('test', ['123'], (v) => (results = v), 'Wrong usage: $command $pattern') 120 | 121 | expect(results).toStrictEqual('Wrong usage: test ') 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

NestJS Shell

3 | 4 |
5 |
6 | 7 |
8 | travis build badge 9 | npm version badge 10 | npm bundle size 11 | license badge 12 | Coverage Status 13 |
14 | 15 |
16 | 17 | ## Description 18 | I wanted to create as simple as possible API without redundant use cases. Which allows you to create a simple command in less than a minute (counting installation time). 19 | Currently, there are a few libraries that provide something similar to this, but they violate your app's execution file with their code and require you to re-run the app's instance every time you want to execute a command. 20 | So I decided to give you full control of where and when shell's instance should start and a way to execute commands in runtime. 21 | 22 | For more examples, go there: https://github.com/bmstefanski/nestjs-shell-example 23 | 24 | 25 | ## Features 26 | 27 | Code sample with command that prints specified parameters to the console 28 | 29 | - [x] 🙉 Non-blocking runtime console 30 | - [x] 🚚 No dependencies 31 | - [x] 🤠 Simple and stable API 32 | - [x] 🛡️ Well tested 33 | - [x] 🖥️ Modifiable error messages and prompt sign 34 | - [x] 🖨️ Elastic output printer (you can write own printer or use any logger you want) 35 | - [x] 📔 Optional, required and varargs parameters 36 | - [ ] 📗 Travis or GitHub Actions based CI 37 | 38 | 39 | ## Installation 40 | 41 | ``` 42 | # production use 43 | $ yarn add nestjs-shell 44 | 45 | # development use 46 | $ yarn add -D nestjs-shell 47 | ``` 48 | 49 | 50 | ## Usage 51 | 52 | ##### Execution and registration 53 | ```typescript 54 | import { ShellModule, ShellFacade } from 'nestjs-shell' 55 | 56 | // `ShellModule` is Global, so please put it only in your main module and it will work flawlessly in others. 57 | @Module({ imports: [ShellModule] }) 58 | export class YourAppMainModule implements OnApplicationBootstrap { 59 | constructor(private readonly shellFacade: ShellFacade) {} 60 | 61 | public async onApplicationBootstrap(): Promise { 62 | // You can use it without passing any arguments and use default configuration or configure it in your own way. 63 | await this.shellFacade.bootstrap() 64 | 65 | // It does not have to be here, you can register components anywhere you want and as many times as you need. 66 | this.shellFacade.registerComponents( 67 | new SayCommandComponent(), 68 | new AnotherSecondTestCommandComponent(new SomeDependency()), 69 | ) 70 | } 71 | } 72 | ``` 73 | 74 | ##### Simple example with required, optional and varargs parameters 75 | ```typescript 76 | import { ShellCommand, ShellComponent } from 'nestjs-shell' 77 | 78 | /* Please do not put @Injectable() or any other decorator that creates a new instance of a class, 79 | it may cause bugs and it is definitely not going to work the way you want. 80 | */ 81 | export class SayCommandComponent extends ShellComponent { 82 | 83 | /* Only `name` property is required, so by default you have no prefix, no description and no pattern 84 | and it works fine! 85 | */ 86 | /* Pattern ideology is simple: 87 | if your parameter name is wrapped with `<` and `>` then it's required 88 | if your parameter name is wrapped with `[` and `]` then it's optional 89 | if there is `@` sign inside any brackets (`[` or `<`) then it's varargs. 90 | Same as in JavaScript varargs, they can only be placed in the last parameter. 91 | */ 92 | @ShellCommand({ 93 | name: 'say', 94 | prefix: '.', 95 | description: 'Sends a message to the console', 96 | pattern: ' [@message]', 97 | }) 98 | public async sayCommand(sender: string, message: string): Promise { 99 | return `${sender} says: ${message || 'Nothing'}` 100 | } 101 | 102 | // There is no limit to the amount of commands in one ShellComponent. 103 | @ShellCommand({ 104 | name: 'said', 105 | prefix: '/', 106 | description: 'Sends a message to the console that has been said', 107 | pattern: ' <@message>', 108 | }) 109 | /* You don't have to keep function's parameters in the same order as pattern ones. 110 | They are applied by name, not order. 111 | */ 112 | public async saidCommand(sender: string, message: string): Promise { 113 | return `${sender} said: ${message}` 114 | } 115 | } 116 | ``` 117 | 118 | 119 | ##### Constructor dependencies 120 | ```typescript 121 | import { ShellCommand, ShellComponent } from 'nestjs-shell' 122 | 123 | export class AnotherSecondTestCommandComponent extends ShellComponent { 124 | constructor(private readonly someDependency: TestDependency) { 125 | super() 126 | } 127 | 128 | /* You can use as much prefixes as you want.. 129 | if you do not specify any then it uses the default, which is '' (empty string) 130 | */ 131 | @ShellCommand({ 132 | name: '.help', 133 | description: 'Displays all commands with description and usage', 134 | }) 135 | public async help(): Promise { 136 | // Method's execution context is ALWAYS set to the actual class instance and so `this` keyword works as expected. 137 | return [ 138 | 'Here are all available commands: ', 139 | '-------------------------------------', 140 | ...this.someDependency, 141 | ...this.shellFacade.getAllCommands().map((command) => { 142 | return `-> ${command.name} ${command.pattern} - ${command.description || 'Description not available'}` 143 | }), 144 | '-------------------------------------', 145 | ].join('\n') 146 | } 147 | } 148 | ``` 149 | 150 | 151 | ## API specification 152 | The library shares its methods through the facade, named `ShellFacade`. In the table below, you can see a brief description of each method. 153 | ```typescript 154 | import { ShellFacade } from 'nestjs-shell' 155 | ``` 156 | 157 | 158 | | Method | Description | Arguments | 159 | | :------------------- | :-------------------------------------------------------------------: | :-------------------------------: | 160 | | `bootstrap` | Enables terminal | `BootstrapOptions` | 161 | | `registerComponents` | Adds command components to the registry | `...components: ShellComponent[]` | 162 | | `getAllCommands` | Returns immutable (or to be more precise: deep copy of a) collection) | naught | 163 | 164 | ```typescript 165 | type BootstrapOptions = { 166 | prompt?: string = '⤳' 167 | messages?: { notFound?: string; wrongUsage?: string } = { 168 | notFound: 'Say what? I might have heard $input', 169 | wrongUsage: 'Wrong usage: $command $pattern', 170 | } 171 | shellPrinter?: ((value: any) => void) = (value: any) => console.log(value) 172 | } 173 | ``` 174 | 175 | 176 | ## Contributions and license 177 | 178 | > **Note:** If you want to contribute, please keep in mind that I don't want to support various use cases, it should remain as simple as it is. So if you desire to improve code rather than add features, then I would greatly appreciate it 🙏🏻🙏🏼🙏🏽🙏🏾🙏🏿. 179 | 180 | Nestjs-shell is [MIT licensed](LICENSE) 181 | 182 | --------------------------------------------------------------------------------