├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── missing_explanation.md └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── kmdr.yaml ├── package-lock.json ├── package.json ├── screenshot.png ├── scripts └── version.ts ├── src ├── Auth.ts ├── Cli.ts ├── CliDecorators.ts ├── Print.ts ├── SettingsManager.ts ├── ThemeManager.ts ├── bin.ts ├── constants.ts ├── errors │ ├── KmdrAuthError.ts │ ├── KmdrError.ts │ ├── KmdrSettingsError.ts │ ├── KmdrThemeError.ts │ └── index.ts ├── files │ ├── settings.json │ └── themes │ │ ├── dia.theme.json │ │ └── greenway.theme.json ├── index.ts ├── interfaces.ts ├── kmdr.ts └── subcommands │ ├── explain │ └── index.ts │ ├── feedback │ └── index.ts │ ├── history │ └── index.ts │ ├── info │ └── index.ts │ ├── login │ └── index.ts │ ├── logout │ └── index.ts │ ├── settings │ └── index.ts │ └── version │ └── index.ts ├── tests ├── Kmdr.test.ts ├── SettingsManager.test.ts └── Theme.test.ts ├── tsconfig.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/missing_explanation.md: -------------------------------------------------------------------------------- 1 | # Report or suggest a missing explanation 2 | 3 | To simply submit a request for an explanation to be created, please provide the 4 | program and command missing, with program version if possible. To provide a 5 | description/if you know the explanation which is missing, please contribute 6 | using the schema template deleting what is not applicable. 7 | 8 | Description schema example you can edit: 9 | 10 | ```yaml 11 | --- 12 | name: kmdr 13 | summary: CLI client for explaning shell commands 14 | link: https://kmdr.sh 15 | version: '0.1' 16 | locale: en 17 | options: 18 | - name: language 19 | short: 20 | - '-l' 21 | long: 22 | - '--language' 23 | summary: Set the language 24 | expectsArg: true 25 | subcommands: 26 | - name: explain 27 | summary: Explain a command 28 | aliases: 29 | - e 30 | - name: config 31 | summary: Configure kmdr on this computer 32 | aliases: 33 | - c 34 | ``` 35 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [8.x, 10.x, 12.x, 13.x, 14.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | npm install 22 | npm run build --if-present 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .DS_Store 4 | dist/ 5 | coverage/ 6 | .vscode/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | coverage/ 3 | tests/ 4 | github/ 5 | tsconfig.json 6 | tslint.json 7 | .prettierrc 8 | .dockerignore 9 | .prettierrc 10 | .github 11 | Dockerfile 12 | screenshot.png 13 | kmdr.yaml 14 | Dockerfile.demo 15 | .vscode/ 16 | scripts/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eddie Ramirez eddie@kmdr.sh 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kmdr-cli ![npm](https://img.shields.io/npm/v/kmdr?color=green&style=flat-square)![npm](https://img.shields.io/npm/dt/kmdr?color=blue&style=flat-square) 2 | 3 | > The CLI tool for learning commands from your terminal 4 | 5 |

6 | 7 |

8 | 9 | `kmdr` provides command explanations for hundreds of programs including `git`, `docker`, `kubectl`,`npm`, `go` and more straight forward programs such as those built into `bash`. See the full list at https://app.kmdr.sh/program. 10 | 11 | ## Installation 12 | 13 | You will need to install the kmdr program and sign-in to begin using kmdr on the CLI. 14 | 15 | ### Requirements 16 | 17 | - Node.js v8.x and above 18 | - A package manager like `npm` or `yarn` 19 | 20 | #### With `npm` 21 | 22 | ```bash 23 | npm install kmdr --global 24 | ``` 25 | 26 | #### With `yarn` 27 | 28 | ```bash 29 | yarn global add kmdr 30 | ``` 31 | 32 | ### Check installation 33 | 34 | Run the command `kmdr` to check if it was correctly installed on your system. 35 | 36 | ```bash 37 | $ kmdr 38 | Usage: kmdr [options] [command] 39 | 40 | The CLI tool for learning commands from your terminal 41 | 42 | Learn more at https://kmdr.sh/ 43 | 44 | Options: 45 | -v, --version output the version number 46 | -h, --help output usage information 47 | 48 | Commands: 49 | explain|e Explain a shell command 50 | info|i Display system-wide information 51 | login|l [email] Log in to kmdr 52 | logout Log out from kmdr 53 | settings|s Adjust options and preferences 54 | version|v Print current version and check for newer release 55 | ``` 56 | 57 | #### Troubleshooting installation 58 | 59 | ##### Command not found: kmdr 60 | 61 | Add the line below to your `.bashrc` or `.zshrc` if using `zsh` 62 | 63 | ```bash 64 | export PATH="$(yarn global bin):$PATH" 65 | ``` 66 | 67 | ### Sign In 68 | 69 | 1. Log in on the kmdr CLI tool 70 | 71 | ```bash 72 | kmdr login 73 | ``` 74 | 75 | 2. Enter your email when prompted 76 | 3. Check your inbox and click on the link provided in the email. 77 | 78 | ## Usage 79 | 80 | ### Explain a command 81 | 82 | Once `kmdr-cli` is installed on your system, enter `kmdr explain` to return a prompt for entering the command you would like explained. 83 | 84 | When the `Enter your command:` prompt is returned, enter the command you would like explained and hit the `Enter` key. 85 | 86 | `kmdr` will return syntax highlighting to assist you in differentiating parts of the command followed by the explanation of each of these parts. 87 | 88 | An example explanation of `git commit -am "Initial commit"` can be seen below. 89 | 90 | ```bash 91 | $ kmdr explain 92 | ✔ Enter your command · git commit -am "Initial Commit" 93 | 94 | git commit -am "Initial Commit" 95 | 96 | DEFINITIONS 97 | 98 | git 99 | The stupid content tracker 100 | commit 101 | Record changes to the repository 102 | -a, --all 103 | Tell the command to automatically stage files that have been modified and deleted 104 | -m, --message "Initial Commit" 105 | Use the given as the commit message 106 | ``` 107 | 108 | ### Examples 109 | 110 | #### Explaining commands with subcommands 111 | 112 | ```bash 113 | $ kmdr explain 114 | ? Enter your command: npm install kmdr@latest --global 115 | 116 | npm install kmdr@latest --global 117 | 118 | DEFINITIONS 119 | 120 | npm 121 | Package manager for the Node JavaScript platform 122 | install 123 | Install a package 124 | kmdr@latest 125 | The CLI tool for learning commands from your terminal 126 | -g, --global 127 | Install the package globally rather than locally 128 | ``` 129 | 130 | #### Explanining commands with grouped options 131 | 132 | ```bash 133 | $ kmdr explain 134 | ? Enter your command: rsync -anv file1 file2 135 | 136 | rsync -anv file1 file2 137 | 138 | DEFINITIONS 139 | 140 | rsync 141 | A fast, versatile, remote (and local) file-copying tool 142 | -a, --archive 143 | This is equivalent to -rlptgoD. 144 | -n, --dry-run 145 | This makes rsync perform a trial run that doesn’t make any changes 146 | (and produces mostly the same output as a real run). 147 | -v, --verbose 148 | This option increases the amount of information you are given during 149 | the transfer. 150 | ``` 151 | 152 | #### Explaining commands with redireciton 153 | 154 | ```bash 155 | $ kmdr explain 156 | ? Enter your command: ls -alh > contents.txt 157 | 158 | ls -alh > contents.txt 159 | 160 | DEFINITIONS 161 | 162 | ls 163 | List directory contents 164 | -a, --all 165 | Do not ignore entries starting with . 166 | -l 167 | Use a long listing format 168 | -h, --human-readable 169 | With -l and/or -s, print human readable sizes (e.g., 1K 234M 2G) 170 | > contents.txt 171 | Redirect stdout to contents.txt. 172 | ``` 173 | 174 | #### Explaining list of commands 175 | 176 | ```bash 177 | $ kmdr explain 178 | ? Enter your command: dmesg | grep 'usb' > output.log 2>error.log 179 | 180 | dmesg | grep 'usb' > output.log 2> error.log 181 | 182 | DEFINITIONS 183 | 184 | dmesg 185 | Print or control the kernel ring buffer 186 | | 187 | A pipe serves the sdout of the previous command as input (stdin) to the next one 188 | grep 189 | Print lines matching a pattern 190 | > output.log 191 | Redirect stdout to output.log. 192 | 2> error.log 193 | Redirect stderr to error.log. 194 | ``` 195 | 196 | So what is the reason for signing in? why should it be included in CLI readme? 197 | 198 | ## Supported programs 199 | 200 | We add new programs every day! See the full list here: https://app.kmdr.sh/program. 201 | 202 | ## Stay tuned for more updates 203 | 204 | - Visit our website 205 | - Follow us on twitter 206 | -------------------------------------------------------------------------------- /kmdr.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: kmdr 3 | summary: The ultimate CLI learning tool 4 | link: https://kmdr.sh 5 | description: 6 | kmdr provides command explanations for hundreds of programs including 7 | git, docker, kubectl,npm, go and more straight forward programs such as those built 8 | into bash 9 | version: 0.2.5 10 | locale: en 11 | options: 12 | - short: 13 | - "-v" 14 | long: 15 | - "--version" 16 | summary: Output the version number 17 | - short: 18 | - "-h" 19 | long: 20 | - "--help" 21 | summary: Output usage information 22 | subcommands: 23 | - name: explain 24 | summary: Explain a command 25 | aliases: 26 | - e 27 | options: 28 | - long: 29 | - "--no-show-syntax" 30 | summary: Hide syntax highlighting 31 | - long: 32 | - "--no-prompt-again" 33 | summary: Do not return prompt for additional explanations 34 | - long: 35 | - "--no-show-related" 36 | summary: Hide related CLI programs 37 | - long: 38 | - "--no-show-examples" 39 | summary: Hide command examples 40 | - long: 41 | - "--help" 42 | short: 43 | - "-h" 44 | summary: Output usage information 45 | - name: upgrade 46 | summary: Check for new releases 47 | aliases: 48 | - u 49 | - name: feedback 50 | summary: Send feedback :) 51 | aliases: 52 | - f 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kmdr", 3 | "version": "1.2.18", 4 | "description": "The CLI tool for learning commands from your terminal", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc --moduleResolution node && cp -r src/files dist/", 8 | "build:version": "ts-node scripts/version.ts", 9 | "test": "jest --verbose", 10 | "start:prod": "NODE_ENV=production ts-node src/bin.ts", 11 | "start:dev": "NODE_ENV=development KMDR_API_ENDPOINT=http://localhost:8081 KMDR_WEBAPP_ENDPOINT=http://localhost:3000 ts-node src/bin.ts" 12 | }, 13 | "bin": { 14 | "kmdr": "dist/bin.js" 15 | }, 16 | "keywords": [ 17 | "shell", 18 | "bash", 19 | "terminal" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/kommandr/kmdr-cli.git" 24 | }, 25 | "author": "Eddie Ramirez (https://github.com/kommandr)", 26 | "homepage": "https://kmdr.sh", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@types/eventsource": "^1.1.5", 30 | "@types/jest": "^24.9.1", 31 | "@types/node": "^12.19.14", 32 | "@types/node-fetch": "^2.5.8", 33 | "jest": "^24.8.0", 34 | "kmdr-parser": "^2.6.1", 35 | "prettier": "^1.17.1", 36 | "ts-jest": "^24.3.0", 37 | "ts-node": "^8.10.2", 38 | "tslint": "^5.18.0", 39 | "tslint-config-prettier": "^1.18.0", 40 | "tslint-config-standard": "^8.0.1", 41 | "typescript": "^3.9.7" 42 | }, 43 | "contributors": [ 44 | { 45 | "name": "Ianeta Hutchinson", 46 | "email": "ianeta.hutch@gmail.com" 47 | } 48 | ], 49 | "jest": { 50 | "testPathIgnorePatterns": [ 51 | "dist/", 52 | "build/", 53 | "src/", 54 | "node_modules/" 55 | ], 56 | "testEnvironment": "node", 57 | "testRegex": "tests/.*\\.(ts|tsx|js)$", 58 | "transform": { 59 | "\\.(ts|tsx)$": "ts-jest" 60 | }, 61 | "globals": { 62 | "ts-jest": { 63 | "diagnostics": { 64 | "ignoreCodes": [ 65 | 2345 66 | ] 67 | } 68 | } 69 | } 70 | }, 71 | "dependencies": { 72 | "chalk": "^2.4.2", 73 | "commander": "^2.20.0", 74 | "cross-fetch": "^3.0.6", 75 | "enquirer": "^2.3.6", 76 | "eventsource": "^1.0.7", 77 | "graphql-request": "^2.0.0", 78 | "kmdr-ast": "3.5.3", 79 | "node-fetch": "^2.6.1", 80 | "ora": "^4.1.1" 81 | }, 82 | "bundledDependencies": [ 83 | "cross-fetch" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExplainDev/kmdr-cli/0d801fe248ee3d5e8966d6dded488b1a21be29c7/screenshot.png -------------------------------------------------------------------------------- /scripts/version.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import os from "os"; 3 | import path from "path"; 4 | 5 | function main() { 6 | const pkgFile = path.join(__dirname, "../", "package.json"); 7 | 8 | try { 9 | const fileContents = fs.readFileSync(pkgFile, { encoding: "utf-8" }); 10 | const parsedContents = JSON.parse(fileContents); 11 | fs.writeFileSync(path.join(__dirname, "..", "VERSION"), parsedContents.version + os.EOL, { 12 | encoding: "utf8", 13 | }); 14 | } catch (err) { 15 | console.error(err); 16 | } 17 | } 18 | 19 | main(); 20 | -------------------------------------------------------------------------------- /src/Auth.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { User } from "./interfaces"; 5 | 6 | export default class Auth { 7 | get kmdrPathExists() { 8 | return fs.existsSync(this.kmdrPath); 9 | } 10 | 11 | get kmdrAuthExists() { 12 | return fs.existsSync(this.kmdrAuthFullPath); 13 | } 14 | 15 | get kmdrAuthFullPath() { 16 | return path.join(this.kmdrPath, this.authFileName); 17 | } 18 | 19 | public static encodeCredentials(email: string, token: string) { 20 | return Buffer.from(`${email}:${token}`).toString("base64"); 21 | } 22 | 23 | /** 24 | * Checks if a string is valid for HTTP Basic Header; 1 non-empty line 25 | * @param token 26 | */ 27 | public static isTokenValidFormat(token: string) { 28 | const trimmedToken = token.trim(); 29 | const lines = trimmedToken.split("\n").length; 30 | 31 | // basic, just check for all spaces, empty or multiline content files 32 | return trimmedToken !== "" && lines == 1; 33 | } 34 | 35 | public token: string = ""; 36 | 37 | protected readonly kmdrPath: string; 38 | protected readonly authFileName = "auth"; 39 | protected currentUser?: User; 40 | 41 | constructor(kmdrPath: string) { 42 | this.kmdrPath = kmdrPath; 43 | this.read(); 44 | } 45 | 46 | public save(email: string, token: string) { 47 | const encoded = Auth.encodeCredentials(email, token); 48 | 49 | try { 50 | if (!this.kmdrPathExists) { 51 | fs.mkdirSync(this.kmdrPath); 52 | } 53 | 54 | fs.writeFileSync(this.kmdrAuthFullPath, encoded + +os.EOL, { 55 | encoding: "ascii", 56 | mode: 0o600, 57 | }); 58 | 59 | this.token = encoded; 60 | } catch (err) { 61 | throw err; 62 | } 63 | } 64 | 65 | public read() { 66 | try { 67 | this.token = fs.readFileSync(this.kmdrAuthFullPath, "utf8"); 68 | } catch (err) { 69 | this.token = ""; 70 | } 71 | } 72 | 73 | public delete() {} 74 | } 75 | -------------------------------------------------------------------------------- /src/Cli.ts: -------------------------------------------------------------------------------- 1 | import "cross-fetch/polyfill"; 2 | import fs from "fs"; 3 | import { GraphQLClient } from "graphql-request"; 4 | import ora from "ora"; 5 | import os from "os"; 6 | import path from "path"; 7 | import Auth from "./Auth"; 8 | import SettingsManager from "./SettingsManager"; 9 | 10 | export default abstract class CLI { 11 | get kmdrDirectoryExists() { 12 | return fs.existsSync(this.KMDR_PATH); 13 | } 14 | 15 | get kmdrAuthFileExists() { 16 | return fs.existsSync(this.KMDR_AUTH_FILE); 17 | } 18 | 19 | public spinner: ora.Ora = ora("Loading..."); 20 | 21 | // These values don't change during the execution of the program. 22 | protected readonly KMDR_WEBAPP_URI: string; 23 | protected readonly KMDR_ENDPOINT_URI: string; 24 | protected readonly KMDR_PATH: string; 25 | protected readonly KMDR_AUTH_FILE: string; 26 | protected readonly KMDR_SETTINGS_FILE: string; 27 | protected readonly LANG?: string; 28 | protected readonly NODE_ENV: string; 29 | protected readonly NODE_PATH?: string; 30 | protected readonly NODE_VERSION: string; 31 | protected readonly OS_ARCH: string; 32 | protected readonly OS_HOME_PATH: string; 33 | protected readonly OS_PLATFORM: string; 34 | protected readonly OS_RELEASE: string; 35 | protected readonly OS_SHELL: string; 36 | protected readonly OS_USERNAME: string; 37 | protected readonly PKG_VERSION!: string; 38 | 39 | protected httpHeaders: { [key: string]: string } = {}; 40 | protected gqlClient!: GraphQLClient; 41 | protected settingsManager!: SettingsManager; 42 | protected auth: Auth; 43 | 44 | constructor() { 45 | this.NODE_ENV = process.env.NODE_ENV || "production"; 46 | this.OS_PLATFORM = os.platform(); 47 | this.OS_RELEASE = os.release(); 48 | this.OS_SHELL = os.userInfo().shell; 49 | this.OS_ARCH = os.arch(); 50 | this.OS_HOME_PATH = os.homedir(); 51 | this.OS_USERNAME = os.userInfo().username; 52 | this.NODE_VERSION = process.versions.node; 53 | this.NODE_PATH = process.env.NODE; 54 | this.LANG = process.env.LANG; 55 | this.KMDR_PATH = path.join(this.OS_HOME_PATH, ".kmdr"); 56 | this.KMDR_AUTH_FILE = path.join(this.KMDR_PATH, "auth"); 57 | this.KMDR_SETTINGS_FILE = path.join(this.KMDR_PATH, "settings.json"); 58 | this.KMDR_WEBAPP_URI = process.env.KMDR_WEBAPP_ENDPOINT || "https://app.kmdr.sh"; 59 | 60 | this.KMDR_ENDPOINT_URI = process.env.KMDR_API_ENDPOINT || "https://api.kmdr.sh"; 61 | this.PKG_VERSION = this.parsePkgJson(); 62 | 63 | this.hookBeforeLoadingSettings(); 64 | this.settingsManager = new SettingsManager(this.KMDR_PATH, this.KMDR_SETTINGS_FILE); 65 | this.hookAfterLoadingSettings(); 66 | 67 | this.hookBeforeLoadingAuth(); 68 | this.auth = new Auth(this.KMDR_PATH); 69 | 70 | let headers: any = { 71 | "X-kmdr-origin": "cli", 72 | "X-kmdr-origin-client-version": this.PKG_VERSION, 73 | }; 74 | 75 | if (this.auth.token !== "") { 76 | headers = { ...headers, authorization: `Basic ${this.auth.token}` }; 77 | } 78 | 79 | this.gqlClient = new GraphQLClient(`${this.KMDR_ENDPOINT_URI}/api/graphql`, { 80 | headers, 81 | }); 82 | 83 | this.hookAfterLoadingAuth(); 84 | } 85 | 86 | // protected gqlClient: GraphQLClient; 87 | public static(encodedCredentials: string) { 88 | const decodedCredentials = Buffer.from(encodedCredentials, "base64").toString(); 89 | return decodedCredentials.split(":"); 90 | } 91 | 92 | public abstract init(): void; 93 | 94 | protected hookBeforeLoadingSettings() { 95 | return; 96 | } 97 | 98 | protected hookAfterLoadingSettings() { 99 | return; 100 | } 101 | 102 | protected hookBeforeLoadingAuth() { 103 | return; 104 | } 105 | 106 | protected hookAfterLoadingAuth() { 107 | return; 108 | } 109 | 110 | private parsePkgJson() { 111 | try { 112 | const pkg = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"); 113 | const parsedPkg = JSON.parse(pkg); 114 | return parsedPkg.version; 115 | } catch { 116 | return "unknown"; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/CliDecorators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentNodeDefinition, 3 | ASTNode, 4 | Decorators, 5 | NodeDefinition, 6 | OptionNodeDefinition, 7 | ProgramNodeDefinition, 8 | SubcommandNodeDefinition, 9 | } from "kmdr-ast"; 10 | import ThemeManager from "./ThemeManager"; 11 | 12 | const DEFAULT_THEME = "GreenWay"; 13 | 14 | export default class CliDecorators implements Decorators { 15 | public theme: ThemeManager; 16 | 17 | constructor(theme: ThemeManager) { 18 | this.theme = theme; 19 | } 20 | 21 | public createDefinition(definition: NodeDefinition): string[] { 22 | let header = ""; 23 | let summary = ""; 24 | 25 | const { type } = definition; 26 | 27 | switch (type) { 28 | case "option": { 29 | const optionDefinition = definition as OptionNodeDefinition; 30 | 31 | const { long, short } = optionDefinition.metadata; 32 | 33 | let allOptions: string[] = []; 34 | 35 | if (short) { 36 | allOptions = [...short]; 37 | } 38 | 39 | if (long) { 40 | allOptions = [...allOptions, ...long]; 41 | } 42 | 43 | const decoratedOptions = allOptions.map((opt) => this.option(opt)).join(", "); 44 | 45 | //header = `${decoratedOptions} ${this.theme.print("tokenKind", type)}`; 46 | header = `${decoratedOptions}`; 47 | 48 | summary = definition.metadata.summary; 49 | break; 50 | } 51 | case "missing_program": 52 | case "program": { 53 | const programDefinition = definition as ProgramNodeDefinition; 54 | const { name } = programDefinition.metadata; 55 | 56 | //header = `${this.program(name)} ${this.theme.print("tokenKind", type)}`; 57 | header = `${this.program(name)}`; 58 | 59 | summary = definition.metadata.summary; 60 | break; 61 | } 62 | case "argument": { 63 | const argumentDefinition = definition as ArgumentNodeDefinition; 64 | const { name, value } = argumentDefinition.metadata; 65 | 66 | header = `${this.argument(value)}`; 67 | summary = definition.metadata.summary; 68 | break; 69 | } 70 | case "subcommand": { 71 | const subcommandDefinition = definition as SubcommandNodeDefinition; 72 | const { name } = subcommandDefinition.metadata; 73 | 74 | header = `${this.subcommand(name)}`; 75 | summary = definition.metadata.summary; 76 | break; 77 | } 78 | case "||": 79 | case "&&": { 80 | header = this.logicalOperator(type); 81 | summary = definition.metadata.summary; 82 | break; 83 | } 84 | case ">": 85 | case ">&": 86 | case ">>": 87 | case "&>": { 88 | header = this.redirect(type); 89 | summary = definition.metadata.summary; 90 | break; 91 | } 92 | case "test_operator": { 93 | header = this.testOperator(definition.metadata.name || ""); 94 | summary = definition.metadata.summary; 95 | break; 96 | } 97 | case ";": { 98 | header = this.semicolon(";"); 99 | summary = definition.metadata.summary; 100 | break; 101 | } 102 | case "file_descriptor": { 103 | header = this.fileDescriptor(definition.metadata.name || ""); 104 | summary = definition.metadata.summary; 105 | break; 106 | } 107 | case "|": { 108 | header = this.pipeline("|"); 109 | summary = definition.metadata.summary; 110 | break; 111 | } 112 | } 113 | 114 | return [header, summary]; 115 | } 116 | 117 | public argument(text: string, _definition?: NodeDefinition) { 118 | return this.theme.print("argument", text); 119 | } 120 | 121 | public arithmeticOperator(text: string, _definition?: NodeDefinition) { 122 | return this.theme.print("operator", text); 123 | } 124 | 125 | public backtick(text: string, _definition?: NodeDefinition) { 126 | return this.theme.print("operator", text); 127 | } 128 | 129 | public bitwiseOperator(text: string, _definition?: NodeDefinition) { 130 | return this.theme.print("operator", text); 131 | } 132 | 133 | public braces(text: string, _definition?: NodeDefinition) { 134 | return this.theme.print("braces", text); 135 | } 136 | 137 | public brackets(text: string, _definition?: NodeDefinition) { 138 | return this.theme.print("brackets", text); 139 | } 140 | 141 | public command(text: string) { 142 | return text; 143 | } 144 | 145 | public comment(text: string, _definition?: NodeDefinition) { 146 | return this.theme.print("comment", text); 147 | } 148 | 149 | public do(text: string, _definition?: NodeDefinition) { 150 | return this.theme.print("keyword", text); 151 | } 152 | 153 | public doubleQuotes(text: string, _definition?: NodeDefinition) { 154 | return this.theme.print("quotes", text); 155 | } 156 | 157 | public done(text: string, _definition?: NodeDefinition) { 158 | return this.theme.print("keyword", text); 159 | } 160 | 161 | public elif(text: string, _definition?: NodeDefinition) { 162 | return this.theme.print("keyword", text); 163 | } 164 | 165 | public else(text: string, _definition?: NodeDefinition) { 166 | return this.theme.print("keyword", text); 167 | } 168 | 169 | public equal(text: string, _definition?: NodeDefinition) { 170 | return text; 171 | } 172 | 173 | public fi(text: string, _definition?: NodeDefinition) { 174 | return this.theme.print("keyword", text); 175 | } 176 | 177 | public fileDescriptor(text: string, _definition?: NodeDefinition) { 178 | return this.theme.print("keyword", text); 179 | } 180 | 181 | public fn(text: string, _definition?: NodeDefinition) { 182 | return this.theme.print("keyword", text); 183 | } 184 | 185 | public for(text: string, _definition?: NodeDefinition) { 186 | return this.theme.print("keyword", text); 187 | } 188 | 189 | public if(text: string, _definition?: NodeDefinition) { 190 | return this.theme.print("keyword", text); 191 | } 192 | 193 | public in(text: string, _definition?: NodeDefinition) { 194 | return this.theme.print("keyword", text); 195 | } 196 | 197 | public logicalOperator(text: string, _definition?: NodeDefinition) { 198 | return this.theme.print("operator", text); 199 | } 200 | 201 | public missingProgram(text: string, _definition?: NodeDefinition) { 202 | return this.theme.print("program", text); 203 | } 204 | 205 | public newLine() { 206 | return "\n"; 207 | } 208 | 209 | public operator(text: string, _definition?: NodeDefinition) { 210 | return this.theme.print("operator", text); 211 | } 212 | 213 | public option(text: string, _definition?: NodeDefinition) { 214 | return this.theme.print("option", text); 215 | } 216 | 217 | public optionArg(text: string, _definition?: NodeDefinition) { 218 | return this.theme.print("argument", text); 219 | } 220 | 221 | public parens(text: string) { 222 | return this.theme.print("parens", text); 223 | } 224 | 225 | public pipeline(text: string, _definition?: NodeDefinition) { 226 | return this.theme.print("operator", text); 227 | } 228 | 229 | public program(text: string, _definition?: NodeDefinition) { 230 | return this.theme.print("program", text); 231 | } 232 | 233 | public redirect(text: string, _definition?: NodeDefinition) { 234 | return this.theme.print("redirect", text); 235 | } 236 | 237 | public relationalOperator(text: string, _definition?: NodeDefinition) { 238 | return this.theme.print("operator", text); 239 | } 240 | 241 | public semicolon(text: string, _definition?: NodeDefinition) { 242 | return this.theme.print("operator", text); 243 | } 244 | 245 | public space() { 246 | return " "; 247 | } 248 | 249 | public subcommand(text: string, _definition?: NodeDefinition) { 250 | return this.theme.print("subcommand", text); 251 | } 252 | 253 | public testOperator(text: string, _definition?: NodeDefinition) { 254 | return this.theme.print("operator", text); 255 | } 256 | 257 | public then(text: string, _definition?: NodeDefinition) { 258 | return this.theme.print("keyword", text); 259 | } 260 | 261 | public variableName(text: string, _definition?: NodeDefinition) { 262 | return this.theme.print("varName", text); 263 | } 264 | 265 | public while(text: string) { 266 | return this.theme.print("keyword", text); 267 | } 268 | 269 | public word(text: string, _definition?: NodeDefinition) { 270 | return text; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Print.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export default class Print { 4 | public static newLine(n = 1) { 5 | let i = 1; 6 | 7 | while (i++ <= n) { 8 | console.log(); 9 | } 10 | } 11 | 12 | public static text(text: string, indent: number = 2) { 13 | const spaces = ` `.repeat(indent); 14 | 15 | console.log(`${spaces}${text}`); 16 | } 17 | 18 | public static header(text: string) { 19 | return console.log(` ${chalk.bold(text.toUpperCase())}`); 20 | } 21 | 22 | public static error(text: string, indent: number = 2) { 23 | const spaces = ` `.repeat(indent); 24 | 25 | console.error(`${spaces}${chalk.red(text)}`); 26 | } 27 | 28 | public static warn(text: string, indent: number = 2) { 29 | const spaces = ` `.repeat(indent); 30 | 31 | console.warn(`${spaces}${chalk.yellow(text)}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SettingsManager.ts: -------------------------------------------------------------------------------- 1 | import { rawListeners } from "commander"; 2 | import fs from "fs"; 3 | import os from "os"; 4 | import path from "path"; 5 | import { KmdrError, KmdrSettingsError } from "./errors"; 6 | import { Settings, SettingsFile, Theme } from "./interfaces"; 7 | import Print from "./Print"; 8 | import ThemeManager from "./ThemeManager"; 9 | 10 | const DEFAULT_THEME: Theme = { 11 | name: "Greenway", 12 | mode: "dark", 13 | palette: { 14 | argument: { 15 | bold: true, 16 | foreground: "#f8f8f2", 17 | italic: true, 18 | }, 19 | braces: { 20 | foreground: "#BD10E0", 21 | }, 22 | brackets: { 23 | foreground: "#BD10E0", 24 | }, 25 | comment: { 26 | foreground: "#9B9B9B", 27 | italic: true, 28 | }, 29 | keyword: { 30 | foreground: "#ff5555", 31 | }, 32 | operator: { 33 | foreground: "#f1fa8c", 34 | }, 35 | option: { 36 | foreground: "#50fa7b", 37 | bold: true, 38 | }, 39 | parens: { 40 | foreground: "#BD10E0", 41 | }, 42 | program: { 43 | foreground: "#50E3C2", 44 | bold: true, 45 | }, 46 | quotes: { 47 | foreground: "#BD10E0", 48 | }, 49 | redirect: { 50 | foreground: "#F5A623", 51 | bold: true, 52 | }, 53 | subcommand: { 54 | foreground: "#F8E71C", 55 | bold: true, 56 | }, 57 | tokenKind: { 58 | background: "#222222", 59 | foreground: "#AA5599", 60 | }, 61 | varName: { 62 | foreground: "#B8E986", 63 | }, 64 | }, 65 | }; 66 | 67 | export default class SettingsManager implements Settings { 68 | public availableThemes: ThemeManager[] = []; 69 | public theme!: ThemeManager; 70 | private settingsPath: string; 71 | private settingsFile: string; 72 | 73 | constructor(settingsPath: string, settingsFile: string) { 74 | this.settingsPath = settingsPath; 75 | this.settingsFile = settingsFile; 76 | 77 | this.loadAllThemes(); 78 | // If directory does not exists, use default settings 79 | if (!fs.existsSync(path.join(this.settingsPath, "settings.json"))) { 80 | this.loadDefault(); 81 | } else { 82 | this.loadFromDisk(); 83 | } 84 | } 85 | 86 | public saveToDisk(newSettings: SettingsFile) { 87 | try { 88 | fs.writeFileSync(this.settingsFile, JSON.stringify(newSettings, null, 2) + os.EOL, { 89 | encoding: "utf8", 90 | mode: 0o640, 91 | }); 92 | } catch (err) { 93 | throw err; 94 | } 95 | } 96 | 97 | private loadAllThemes() { 98 | const themesPath = path.join(__dirname, "files", "themes"); 99 | 100 | const themeFiles = fs.readdirSync(themesPath); 101 | 102 | for (const file of themeFiles) { 103 | const filePath = path.join(themesPath, file); 104 | 105 | try { 106 | const fileContents = fs.readFileSync(filePath, { encoding: "utf-8" }); 107 | const parsedContents = JSON.parse(fileContents) as Theme; 108 | const theme = new ThemeManager(parsedContents); 109 | if (theme) { 110 | this.availableThemes.push(theme); 111 | } 112 | } catch (err) { 113 | throw err; 114 | } 115 | } 116 | } 117 | 118 | private loadDefault() { 119 | this.theme = new ThemeManager(DEFAULT_THEME); 120 | } 121 | 122 | private loadFromDisk() { 123 | try { 124 | const file = fs.readFileSync(this.settingsFile, "utf8"); 125 | 126 | if (file === "") { 127 | this.theme = new ThemeManager(DEFAULT_THEME); 128 | } else { 129 | const parsedFile = JSON.parse(file) as SettingsFile; 130 | 131 | if (parsedFile.theme.trim() === "") { 132 | throw new KmdrError("KMDR_CLI_INVALID_THEME_NAME", 201, `Settings file is invalid`); 133 | } 134 | 135 | this.theme = this.loadTheme(parsedFile.theme) || new ThemeManager(DEFAULT_THEME); 136 | } 137 | } catch (err) { 138 | if (err instanceof SyntaxError) { 139 | Print.error( 140 | `Settings file is invalid. Delete file ${this.settingsFile} and run "kmdr settings" again`, 141 | ); 142 | Print.error(err.message); 143 | Print.newLine(); 144 | process.exit(1); 145 | } else if (err.code === "EACCES") { 146 | Print.error( 147 | `Could not open file ${this.settingsFile} for read. Make sure it has the right Read permissions for current user`, 148 | ); 149 | Print.error(err.message); 150 | process.exit(1); 151 | } else if (err.code === "ENOENT") { 152 | Print.error(err.message); 153 | process.exit(1); 154 | } else { 155 | throw err; 156 | } 157 | } 158 | } 159 | 160 | private loadTheme(name: string) { 161 | for (const theme of this.availableThemes) { 162 | if (theme.name === name) { 163 | return theme; 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/ThemeManager.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { PaletteOptions, Theme, ThemePalette } from "./interfaces"; 3 | 4 | const defaultPaletteOptions: PaletteOptions = { 5 | foreground: "#FFFFFF", 6 | }; 7 | 8 | export default class ThemeManager implements Theme { 9 | public readonly name: string; 10 | public readonly mode?: string; 11 | public readonly palette: ThemePalette; 12 | 13 | constructor(theme: Theme) { 14 | this.name = theme.name; 15 | this.mode = theme.mode; 16 | this.palette = theme.palette; 17 | } 18 | 19 | public print(kind: string, text: string) { 20 | const paletteOptions = kind in this.palette ? this.palette[kind] : defaultPaletteOptions; 21 | 22 | let styled = chalk.hex(paletteOptions.foreground); 23 | 24 | if (paletteOptions.background) { 25 | styled = styled.bgHex(paletteOptions.background); 26 | } 27 | 28 | let styledText = styled(text); 29 | 30 | if (paletteOptions.underline) { 31 | styledText = styled.underline(styledText); 32 | } 33 | 34 | if (paletteOptions.bold) { 35 | styledText = styled.bold(styledText); 36 | } 37 | 38 | if (paletteOptions.italic) { 39 | styledText = styled.italic(styledText); 40 | } 41 | 42 | return styledText; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import main from "./index"; 4 | main(); 5 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXIT_STATUS = { 2 | /** COULD NOT REACH THE API BECAUSE OF A NETWORK ISSUE (OR API IS DOWN) */ 3 | API_UNREACHABLE: 30, 4 | /** Auth file contents look invalid (multiple lines or empty) $HOME/.kmdr/auth */ 5 | AUTH_FILE_INVALID: 10, 6 | /** Auth file does not exist $HOME/.kmdr/auth */ 7 | AUTH_FILE_NOT_PRESENT: 11, 8 | /** Auth path ($HOME/.kmdr) access forbidden (permission denied) */ 9 | AUTH_PATH_EACCESS: 12, 10 | /** Generic */ 11 | GENERIC: 1, 12 | /** The token expired. It occurs when user does not click the login link. */ 13 | TOKEN_EXPIRED: 20, 14 | /** Could not find a valid session in the server */ 15 | USER_NOT_AUTHENTICATED: 40, 16 | }; 17 | -------------------------------------------------------------------------------- /src/errors/KmdrAuthError.ts: -------------------------------------------------------------------------------- 1 | import KmdrError from "./KmdrError"; 2 | 3 | export default class KmdrAuthError extends KmdrError { 4 | constructor(message: string) { 5 | super("AUTH_ERROR", 100, message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/errors/KmdrError.ts: -------------------------------------------------------------------------------- 1 | export default class KmdrError extends Error { 2 | public code: string; 3 | public errno: number; 4 | public message: string; 5 | 6 | constructor(code: string, errno: number, message: string) { 7 | super(); 8 | this.code = code; 9 | this.errno = errno; 10 | this.message = message; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/KmdrSettingsError.ts: -------------------------------------------------------------------------------- 1 | import KmdrError from "./KmdrError"; 2 | 3 | export default class KmdrSettingsError extends KmdrError { 4 | constructor(message: string) { 5 | super("SETTINGS_ERROR", 100, message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/errors/KmdrThemeError.ts: -------------------------------------------------------------------------------- 1 | import KmdrError from "./KmdrError"; 2 | 3 | export default class KmdrThemeError extends KmdrError { 4 | constructor(message: string) { 5 | super("THEMEERROR", 200, message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import KmdrAuthError from "./KmdrAuthError"; 2 | import KmdrError from "./KmdrError"; 3 | import KmdrThemeError from "./KmdrThemeError"; 4 | import KmdrSettingsError from "./KmdrSettingsError"; 5 | 6 | export { KmdrAuthError, KmdrError, KmdrThemeError, KmdrSettingsError }; 7 | -------------------------------------------------------------------------------- /src/files/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "greenway" 3 | } 4 | -------------------------------------------------------------------------------- /src/files/themes/dia.theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dia", 3 | "mode": "light", 4 | "palette": { 5 | "argument": { 6 | "bold": true, 7 | "foreground": "#575F66", 8 | "italic": true 9 | }, 10 | "comment": { 11 | "foreground": "#ABB0B6" 12 | }, 13 | "function": { 14 | "foreground": "#F2AE49" 15 | }, 16 | "keyword": { 17 | "foreground": "#ff5555" 18 | }, 19 | "operator": { 20 | "foreground": "#f1fa8c" 21 | }, 22 | "option": { 23 | "foreground": "#399EE6", 24 | "bold": true 25 | }, 26 | "program": { 27 | "foreground": "#F2AE49", 28 | "bold": true 29 | }, 30 | "subcommand": { 31 | "foreground": "#4CBF99", 32 | "bold": true 33 | }, 34 | "varName": { 35 | "foreground": "#F07171", 36 | "bold": true 37 | }, 38 | "tokenKind": { 39 | "background": "#222222", 40 | "foreground": "#AA5599" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/files/themes/greenway.theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Greenway", 3 | "mode": "dark", 4 | "palette": { 5 | "argument": { 6 | "bold": true, 7 | "foreground": "#f8f8f2", 8 | "italic": true 9 | }, 10 | "braces": { 11 | "foreground": "#BD10E0" 12 | }, 13 | "brackets": { 14 | "foreground": "#BD10E0" 15 | }, 16 | "comment": { 17 | "foreground": "#9B9B9B", 18 | "italic": true 19 | }, 20 | "keyword": { 21 | "foreground": "#ff5555" 22 | }, 23 | "operator": { 24 | "foreground": "#f1fa8c" 25 | }, 26 | "option": { 27 | "foreground": "#50fa7b", 28 | "bold": true 29 | }, 30 | "parens": { 31 | "foreground": "#BD10E0" 32 | }, 33 | "program": { 34 | "foreground": "#50E3C2", 35 | "bold": true 36 | }, 37 | "quotes": { 38 | "foreground": "#BD10E0" 39 | }, 40 | "redirect": { 41 | "foreground": "#F5A623", 42 | "bold": true 43 | }, 44 | "subcommand": { 45 | "foreground": "#F8E71C", 46 | "bold": true 47 | }, 48 | "tokenKind": { 49 | "background": "#222222", 50 | "foreground": "#AA5599" 51 | }, 52 | "varName": { 53 | "foreground": "#B8E986" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import arg from "commander"; 2 | import KMDR from "./Kmdr"; 3 | import Explain from "./subcommands/explain"; 4 | import Info from "./subcommands/info"; 5 | import Login from "./subcommands/login"; 6 | import Logout from "./subcommands/logout"; 7 | import Settings from "./subcommands/settings"; 8 | import Version from "./subcommands/version"; 9 | import History from "./subcommands/history"; 10 | 11 | export default function main() { 12 | async function initExplain() { 13 | const explain = new Explain(); 14 | await explain.init(); 15 | } 16 | 17 | async function initInfo() { 18 | const info = new Info(); 19 | await info.init(); 20 | } 21 | 22 | async function initKmdr() { 23 | const kmdr = new KMDR(); 24 | await kmdr.init(); 25 | } 26 | 27 | async function initHistory() { 28 | const history = new History(); 29 | await history.init(); 30 | } 31 | 32 | async function initLogin(email: string) { 33 | const login = new Login(email); 34 | await login.init(); 35 | } 36 | 37 | async function initLogout() { 38 | const logout = new Logout(); 39 | await logout.init(); 40 | } 41 | 42 | async function initSettings() { 43 | const settings = new Settings(); 44 | await settings.init(); 45 | } 46 | 47 | async function initVersion() { 48 | const version = new Version(); 49 | await version.init(); 50 | } 51 | const welcomeMsg = `The CLI tool for learning commands from your terminal\n\nLearn more at https://kmdr.sh/`; 52 | 53 | arg.description(welcomeMsg).option("-v, --version", "Print current version", async () => { 54 | await initKmdr(); 55 | }); 56 | 57 | arg.command("explain").alias("e").description("Explain a shell command").action(initExplain); 58 | arg.command("info").alias("i").description("Display system-wide information").action(initInfo); 59 | arg.command("login [email]").alias("l").description("Log in to kmdr").action(initLogin); 60 | arg.command("logout").description("Log out from kmdr").action(initLogout); 61 | arg.command("history").alias("h").description("View command history").action(initHistory); 62 | arg 63 | .command("settings") 64 | .alias("s") 65 | .description("Adjust options and preferences") 66 | .action(initSettings); 67 | arg 68 | .command("version") 69 | .alias("v") 70 | .description("Print current version and check for newer releases") 71 | .action(initVersion); 72 | 73 | arg.parse(process.argv); 74 | 75 | if (process.argv.length < 3) { 76 | arg.help(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Command, Program } from "kmdr-parser"; 2 | import { UserInfo } from "os"; 3 | 4 | export interface Theme { 5 | name: string; 6 | mode?: string; 7 | palette: ThemePalette; 8 | } 9 | 10 | interface ExplainData { 11 | explain: Explain | null; 12 | } 13 | 14 | export interface Explain { 15 | query: string; 16 | ast: string; 17 | examples: Command[]; 18 | relatedPrograms: Program[]; 19 | } 20 | 21 | export interface ConsoleAnswers { 22 | [key: string]: string; 23 | } 24 | 25 | export interface AuthCredentials { 26 | username: string; 27 | token: string; 28 | } 29 | 30 | export interface ExplainConfig { 31 | promptAgain: boolean; 32 | showRelatedPrograms: boolean; 33 | showSyntax: boolean; 34 | showExamples: boolean; 35 | } 36 | 37 | interface ExplainFeedbackData { 38 | answer: string; 39 | comment: string; 40 | } 41 | 42 | interface CommandData { 43 | summary: string; 44 | command: string; 45 | totalViews: number; 46 | createdAt: string; 47 | } 48 | 49 | interface RelatedProgramsData { 50 | relatedPrograms: Program[]; 51 | } 52 | 53 | interface LatestCliReleaseData { 54 | latestCliRelease: LatestCliRelease; 55 | } 56 | 57 | interface LatestCliRelease { 58 | isCliVersionCurrent: boolean; 59 | latestRelease: CliVersion; 60 | } 61 | 62 | interface CliVersion { 63 | url: string; 64 | body: string; 65 | tagName: string; 66 | preRelase: string; 67 | } 68 | 69 | export interface ConsolePrintOptions { 70 | color?: string; 71 | margin?: number; 72 | appendNewLine?: boolean; 73 | prependNewLine?: boolean; 74 | wrap?: boolean; 75 | } 76 | 77 | export interface GraphQLResponse { 78 | data: any; 79 | } 80 | 81 | interface FeedbackData { 82 | createFeedback: Feedback; 83 | } 84 | 85 | interface Feedback { 86 | message: string; 87 | } 88 | 89 | export interface LoginIdResponse { 90 | loginId: string; 91 | } 92 | 93 | export interface GetProgramAstResponse { 94 | getProgramAST: ProgramAst; 95 | } 96 | 97 | export interface User { 98 | name: string; 99 | email: string; 100 | locale: string; 101 | username: string; 102 | location: string; 103 | isPro: boolean; 104 | } 105 | 106 | export interface CurrentUserReponse { 107 | currentUser: User; 108 | } 109 | 110 | interface Feedback { 111 | status: string; 112 | } 113 | 114 | export interface SaveFeedbackResponse { 115 | saveFeedback: Feedback; 116 | } 117 | 118 | interface ProgramAst { 119 | ast: string; 120 | definitions: string; 121 | perf?: string; 122 | sessionId: string; 123 | permalink: string; 124 | commandId: string; 125 | } 126 | 127 | export interface ThemePalette { 128 | argument: PaletteOptions; 129 | comment: PaletteOptions; 130 | keyword: PaletteOptions; 131 | operator: PaletteOptions; 132 | option: PaletteOptions; 133 | program: PaletteOptions; 134 | subcommand: PaletteOptions; 135 | brackets: PaletteOptions; 136 | braces: PaletteOptions; 137 | [key: string]: PaletteOptions; 138 | } 139 | 140 | export interface PaletteOptions { 141 | background?: string; 142 | foreground: string; 143 | underline?: boolean; 144 | italic?: boolean; 145 | bold?: boolean; 146 | } 147 | 148 | export interface Settings { 149 | theme: Theme; 150 | } 151 | 152 | export interface SettingsFile { 153 | theme: string; 154 | } 155 | -------------------------------------------------------------------------------- /src/kmdr.ts: -------------------------------------------------------------------------------- 1 | import CLI from "./Cli"; 2 | import Print from "./Print"; 3 | 4 | class KMDR extends CLI { 5 | public async init() { 6 | Print.text(this.PKG_VERSION); 7 | } 8 | } 9 | 10 | export default KMDR; 11 | -------------------------------------------------------------------------------- /src/subcommands/explain/index.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from "enquirer"; 2 | import { ClientError } from "graphql-request"; 3 | import { Highlight, NodeDefinition, Tree } from "kmdr-ast"; 4 | import Auth from "../../Auth"; 5 | import CLI from "../../Cli"; 6 | import CliDecorators from "../../CliDecorators"; 7 | import { EXIT_STATUS } from "../../constants"; 8 | import { KmdrAuthError } from "../../errors"; 9 | import { GetProgramAstResponse, SaveFeedbackResponse } from "../../interfaces"; 10 | import Print from "../../Print"; 11 | import chalk from "chalk"; 12 | 13 | interface ExplainInputQuery { 14 | source: string; 15 | } 16 | 17 | interface NextActionAnswer { 18 | action: string; 19 | } 20 | 21 | interface FeedbackAnswer { 22 | feedback: string; 23 | } 24 | 25 | export default class Explain extends CLI { 26 | private decorators: CliDecorators; 27 | 28 | constructor() { 29 | super(); 30 | this.decorators = new CliDecorators(this.settingsManager.theme); 31 | } 32 | 33 | public async init() { 34 | let repeat = false; 35 | 36 | try { 37 | do { 38 | const rawSource = await this.promptSource(); 39 | const trimmedSource = rawSource.trim(); 40 | 41 | if (trimmedSource === "") { 42 | Print.error("Enter a non-empty query"); 43 | Print.newLine(); 44 | repeat = true; 45 | continue; 46 | } 47 | 48 | // Print a new line after prompting the user 49 | Print.newLine(); 50 | this.spinner?.start("Analyzing the command..."); 51 | const programAst = await this.getProgramAST(trimmedSource); 52 | this.spinner?.stop(); 53 | const { ast, definitions, perf, sessionId, permalink, commandId } = programAst; 54 | const parsedAST = JSON.parse(ast); 55 | const tree = new Tree(parsedAST); 56 | const parsedDefinitions: NodeDefinition[] = JSON.parse(definitions); 57 | 58 | const highlight = new Highlight(this.decorators, "cli"); 59 | const decoratedNodes = highlight.source(trimmedSource, tree, parsedDefinitions); 60 | const decoratedString = decoratedNodes.join(""); 61 | 62 | // Syntax highlighting 63 | Print.text(decoratedString, 4); 64 | Print.newLine(); 65 | 66 | Print.header("Definitions"); 67 | Print.newLine(); 68 | 69 | if (parsedDefinitions.length === 0) { 70 | Print.error("Could not find any definitions in your command"); 71 | } 72 | 73 | for (const def of parsedDefinitions) { 74 | if (def.type === "optionArg") continue; 75 | const [header, summary] = this.decorators.createDefinition(def); 76 | Print.text(header, 4); 77 | Print.text(summary, 6); 78 | } 79 | 80 | Print.newLine(); 81 | 82 | const detailsUrl = this.detailsURL(commandId); 83 | Print.text(`Open in browser ${detailsUrl}`); 84 | Print.newLine(); 85 | 86 | // what do you want to do next? 87 | const action = await this.promptNextAction(); 88 | 89 | switch (action) { 90 | case "ask": { 91 | repeat = true; 92 | break; 93 | } 94 | case "feedback": { 95 | const answer = await this.promptFeedback(); 96 | this.spinner?.start("Sending your feedback..."); 97 | const status = await this.saveFeedback(answer, sessionId); 98 | 99 | if (status) { 100 | this.spinner?.succeed("Thank you!"); 101 | } else { 102 | this.spinner?.fail("Sorry, we couldn't save your feedback this time."); 103 | } 104 | repeat = true; 105 | break; 106 | } 107 | case "exit": { 108 | repeat = false; 109 | break; 110 | } 111 | } 112 | } while (repeat); 113 | } catch (err) { 114 | if (err instanceof KmdrAuthError) { 115 | this.spinner?.fail(err.message); 116 | Print.error(""); 117 | process.exit(EXIT_STATUS.USER_NOT_AUTHENTICATED); 118 | } else if (err.code === "ECONNREFUSED") { 119 | this.spinner?.fail("Could not reach the API registry. Are you connected to the internet?"); 120 | Print.error(err); 121 | Print.error(""); 122 | process.exit(EXIT_STATUS.API_UNREACHABLE); 123 | } else if (err !== "") { 124 | process.exit(EXIT_STATUS.GENERIC); 125 | } 126 | } 127 | } 128 | 129 | protected async hookAfterLoadingAuth() { 130 | if (!this.kmdrAuthFileExists) { 131 | Print.error(`No login detected on this machine. Sign in to continue.\n`); 132 | Print.error(`$ kmdr login`); 133 | Print.newLine(); 134 | process.exit(EXIT_STATUS.AUTH_FILE_NOT_PRESENT); 135 | } else if (!Auth.isTokenValidFormat(this.auth.token)) { 136 | Print.error(`File ${this.KMDR_AUTH_FILE} is invalid. Delete it and sign in again.\n`); 137 | Print.error(`$ rm ${this.KMDR_AUTH_FILE} && kmdr login`); 138 | Print.newLine(); 139 | process.exit(EXIT_STATUS.AUTH_FILE_INVALID); 140 | } 141 | } 142 | 143 | private async getProgramAST(source: string) { 144 | const gqlQuery = /* GraphQL */ ` 145 | query getProgramAST($source: String!) { 146 | getProgramAST(source: $source) { 147 | ast 148 | definitions 149 | perf 150 | sessionId 151 | permalink 152 | commandId 153 | } 154 | } 155 | `; 156 | 157 | try { 158 | const data = await this.gqlClient.request(gqlQuery, { source }); 159 | return data.getProgramAST; 160 | } catch (err) { 161 | if (err instanceof ClientError && err.response.status === 401) { 162 | throw new KmdrAuthError("You are not logged in. Run `kmdr login` to sign in."); 163 | } else { 164 | throw err; 165 | } 166 | } 167 | } 168 | 169 | private async saveFeedback(answer: string, sessionId: string) { 170 | const gqlMutation = ` 171 | mutation saveFeedback($answer: String!, $sessionId: String!) { 172 | saveFeedback(answer: $answer, sessionId: $sessionId) { 173 | status 174 | } 175 | }`; 176 | 177 | try { 178 | const data = await this.gqlClient.request(gqlMutation, { 179 | answer, 180 | sessionId, 181 | }); 182 | return data.saveFeedback; 183 | } catch (err) { 184 | throw err; 185 | } 186 | } 187 | 188 | private async promptFeedback() { 189 | const feedbackQuestions = { 190 | choices: [ 191 | { message: "Yes", value: "yes" }, 192 | { message: "No", value: "no" }, 193 | ], 194 | message: "Was this helpful?", 195 | name: "feedback", 196 | type: "select", 197 | }; 198 | try { 199 | const answer = await prompt(feedbackQuestions); 200 | return answer.feedback; 201 | } catch (err) { 202 | throw err; 203 | } 204 | } 205 | 206 | private async promptNextAction() { 207 | const nextQuestions = { 208 | choices: [ 209 | { 210 | message: "Explain more commands", 211 | value: "ask", 212 | }, 213 | { message: "Was this helpful?", value: "feedback" }, 214 | { role: "separator" }, 215 | { message: "Exit (or press Ctrl+c)", value: "exit" }, 216 | ], 217 | message: "What do you want to do?", 218 | name: "action", 219 | type: "select", 220 | }; 221 | try { 222 | const answer = await prompt(nextQuestions); 223 | return answer.action; 224 | } catch (err) { 225 | throw err; 226 | } 227 | } 228 | 229 | private async promptSource() { 230 | const inputQuestion = { 231 | message: "Enter your command", 232 | name: "source", 233 | type: "input", 234 | }; 235 | 236 | let source = ""; 237 | 238 | try { 239 | const input = await prompt(inputQuestion); 240 | source = input.source; 241 | } catch (err) { 242 | throw err; 243 | } 244 | 245 | return source; 246 | } 247 | 248 | private detailsURL(id: string) { 249 | const url = 250 | this.NODE_ENV === "production" 251 | ? `https://h.kmdr.sh/${id}` 252 | : `${this.KMDR_WEBAPP_URI}/history/${id}`; 253 | 254 | return chalk.blueBright.bold.underline(url); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/subcommands/feedback/index.ts: -------------------------------------------------------------------------------- 1 | import CLI from "../../Cli"; 2 | 3 | export default class Feedback extends CLI { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | public async init() { 9 | console.log("Feedback"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/subcommands/history/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import fetch from "node-fetch"; 3 | import CLI from "../../Cli"; 4 | import { KmdrAuthError } from "../../errors"; 5 | import Print from "../../Print"; 6 | 7 | export default class HIstory extends CLI { 8 | constructor() { 9 | super(); 10 | } 11 | 12 | public async init() { 13 | Print.text(`Go to ${this.KMDR_WEBAPP_URI}/history to view your command history`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/subcommands/info/index.ts: -------------------------------------------------------------------------------- 1 | import CLI from "../../Cli"; 2 | import Print from "../../Print"; 3 | 4 | export default class Info extends CLI { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | public async init() { 10 | Print.text(`Kmdr:`); 11 | Print.text(`Version: ${this.PKG_VERSION}`, 4); 12 | Print.text(`Registry Endpoint: ${this.KMDR_ENDPOINT_URI}`, 4); 13 | Print.text(`Settings directory: ${this.KMDR_PATH}`, 4); 14 | Print.text(`Current Theme: ${this.settingsManager.theme.name}`, 4); 15 | Print.text(`Node.js:`); 16 | Print.text(`Version: ${this.NODE_VERSION}`, 4); 17 | Print.text(`Path: ${this.NODE_PATH}`, 4); 18 | 19 | // OS 20 | Print.text(`OS:`); 21 | Print.text(`Architecture: ${this.OS_ARCH}`, 4); 22 | Print.text(`User: ${this.OS_USERNAME}`, 4); 23 | Print.text(`Home directory: ${this.OS_HOME_PATH}`, 4); 24 | Print.text(`Platform: ${this.OS_PLATFORM}`, 4); 25 | Print.text(`Release: ${this.OS_RELEASE}`, 4); 26 | Print.text(`Shell: ${this.OS_SHELL}`, 4); 27 | Print.text(`Locale: ${this.LANG}`, 4); 28 | Print.newLine(); 29 | } 30 | 31 | // protected pre() { 32 | // return; 33 | // } 34 | } 35 | -------------------------------------------------------------------------------- /src/subcommands/login/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { EACCES } from "constants"; 3 | import { prompt } from "enquirer"; 4 | import EventSource from "eventsource"; 5 | import fs from "fs"; 6 | import { ClientError } from "graphql-request"; 7 | import fetch from "node-fetch"; 8 | import os from "os"; 9 | import Auth from "../../Auth"; 10 | import CLI from "../../Cli"; 11 | import { EXIT_STATUS } from "../../constants"; 12 | import KmdrAuthError from "../../errors/KmdrAuthError"; 13 | import { CurrentUserReponse, LoginIdResponse, User } from "../../interfaces"; 14 | import Print from "../../Print"; 15 | 16 | interface EmailInput { 17 | email: string; 18 | } 19 | 20 | export default class Login extends CLI { 21 | private email!: string; 22 | private token!: string; 23 | private eventSource!: EventSource; 24 | 25 | constructor(email?: string) { 26 | super(); 27 | 28 | if (email) { 29 | this.email = email; 30 | } 31 | 32 | this.eventSourceMessage = this.eventSourceMessage.bind(this); 33 | } 34 | 35 | public async init() { 36 | try { 37 | const currentUser = await this.getCurrentUser(); 38 | 39 | if (currentUser) { 40 | this.spinner.succeed("You're logged in!"); 41 | Print.newLine(); 42 | this.printUserInfo(currentUser); 43 | Print.newLine(); 44 | process.exit(); 45 | } 46 | 47 | Print.text("Hi, welcome to kmdr-cli! 👋"); 48 | Print.newLine(); 49 | 50 | if (!this.email) { 51 | Print.text( 52 | `We use email-based, passwordless authentication. Enter your email address and you will receive a one-time login link`, 53 | ); 54 | Print.newLine(); 55 | 56 | this.email = await this.promptEmail(); 57 | } else { 58 | Print.text( 59 | `We use email-based, passwordless authentication. You will receive a one-time login link.`, 60 | ); 61 | } 62 | 63 | Print.newLine(); 64 | 65 | await this.watchLoginEvent(); 66 | } catch (err) { 67 | if (err instanceof KmdrAuthError) { 68 | this.spinner.fail(err.message); 69 | Print.newLine(); 70 | Print.text(`$ rm ${this.KMDR_AUTH_FILE} && kmdr login`); 71 | Print.newLine(); 72 | process.exit(EXIT_STATUS.USER_NOT_AUTHENTICATED); 73 | } else if (err.code === "ECONNREFUSED") { 74 | this.spinner.fail("Could not reach the API registry. Are you connected to the internet?"); 75 | Print.error(err); 76 | Print.error(""); 77 | process.exit(EXIT_STATUS.API_UNREACHABLE); 78 | } else if (err !== "") { 79 | this.spinner.fail("An error occurred"); 80 | Print.error(""); 81 | Print.error(err); 82 | Print.error(""); 83 | process.exit(EXIT_STATUS.GENERIC); 84 | } 85 | } 86 | } 87 | 88 | protected async hookAfterLoadingAuth() { 89 | if (!this.kmdrAuthFileExists) { 90 | return; 91 | } 92 | 93 | if (!Auth.isTokenValidFormat(this.auth.token)) { 94 | this.spinner.fail(`File ${this.KMDR_AUTH_FILE} is invalid. Delete it and try again.`); 95 | Print.error(""); 96 | Print.error(`$ rm ${this.KMDR_AUTH_FILE} && kmdr login`); 97 | Print.error(""); 98 | process.exit(EXIT_STATUS.AUTH_FILE_INVALID); 99 | } 100 | } 101 | 102 | /** 103 | * Prints information about the user, e.g. email, username 104 | * 105 | * @param user 106 | */ 107 | protected printUserInfo(user: User) { 108 | Print.text(`Email: ${user.email}`); 109 | 110 | if (!user.username) { 111 | Print.text(`Username: You haven't picked a username yet`); 112 | Print.newLine(); 113 | Print.text(`Complete your registration at ${this.KMDR_WEBAPP_URI}/welcome`); 114 | } else { 115 | Print.text(`Username: ${user.username}`); 116 | Print.newLine(); 117 | Print.text(`Manage your account at ${this.KMDR_WEBAPP_URI}/settings`); 118 | } 119 | } 120 | 121 | private async eventSourceMessage(evt: MessageEvent) { 122 | const { data } = evt; 123 | 124 | switch (data) { 125 | case "active": { 126 | try { 127 | this.auth.save(this.email, this.token); 128 | this.spinner.succeed("You are now logged in!"); 129 | 130 | this.gqlClient.setHeader("authorization", `Basic ${this.auth.token}`); 131 | const currentUser = await this.getCurrentUser(); 132 | Print.newLine(); 133 | this.printUserInfo(currentUser); 134 | Print.newLine(); 135 | Print.text("Run `kmdr explain` to get instant command definitions. "); 136 | Print.newLine(); 137 | } catch (err) { 138 | this.spinner.fail("An error occurred"); 139 | Print.newLine(); 140 | if (err instanceof KmdrAuthError) { 141 | Print.error(err.message); 142 | Print.newLine(); 143 | Print.text(`$ rm ${this.KMDR_AUTH_FILE} && kmdr login`); 144 | Print.newLine(); 145 | process.exit(EXIT_STATUS.USER_NOT_AUTHENTICATED); 146 | } else if (err.code === "EACCES") { 147 | Print.error(`Could not read or create directory ${this.KMDR_PATH}`); 148 | Print.error(err.message); 149 | Print.newLine(); 150 | process.exit(EXIT_STATUS.AUTH_PATH_EACCESS); 151 | } 152 | } 153 | 154 | this.eventSource.close(); 155 | break; 156 | } 157 | case "pending": { 158 | this.spinner.color = "white"; 159 | this.spinner.spinner = "dots2"; 160 | this.spinner.start( 161 | `Check your inbox and click on the link provided. The link in your email will be valid for 10 minutes.\n\n ${chalk.bold( 162 | "DO NOT close the terminal or exit this program", 163 | )}\n`, 164 | ); 165 | 166 | break; 167 | } 168 | case "expired": { 169 | this.spinner.fail("The link expired :("); 170 | Print.newLine(); 171 | this.eventSource.close(); 172 | process.exit(EXIT_STATUS.TOKEN_EXPIRED); 173 | break; 174 | } 175 | case "logout": { 176 | this.spinner?.fail("You're logged out."); 177 | Print.newLine(); 178 | this.eventSource.close(); 179 | break; 180 | } 181 | } 182 | } 183 | 184 | private async getCurrentUser() { 185 | const gqlQuery = /* GraphQL */ ` 186 | query currentUser { 187 | currentUser { 188 | username 189 | email 190 | name 191 | location 192 | locale 193 | isPro 194 | } 195 | } 196 | `; 197 | 198 | try { 199 | const data = await this.gqlClient.request(gqlQuery); 200 | return data.currentUser; 201 | } catch (err) { 202 | if ((err instanceof ClientError && err.response.status === 401) || err instanceof TypeError) { 203 | throw new KmdrAuthError( 204 | `The token in ${this.KMDR_AUTH_FILE} is invalid. Manually delete the file and log in again`, 205 | ); 206 | } 207 | 208 | throw err; 209 | } 210 | } 211 | 212 | private async watchLoginEvent() { 213 | const { email } = this; 214 | 215 | this.spinner.start("Contacting server..."); 216 | 217 | try { 218 | const res = await fetch(`${this.KMDR_ENDPOINT_URI}/login`, { 219 | body: JSON.stringify({ email }), 220 | headers: { 221 | "Content-Type": "application/json", 222 | "X-kmdr-origin": "cli", 223 | "X-kmdr-origin-client-version": this.PKG_VERSION, 224 | }, 225 | method: "POST", 226 | }); 227 | 228 | if (res.ok) { 229 | this.spinner.succeed(`Email sent to ${this.email}`); 230 | Print.newLine(); 231 | const loginResponse: LoginIdResponse = await res.json(); 232 | 233 | this.token = loginResponse.loginId; 234 | 235 | this.eventSource = new EventSource( 236 | `${this.KMDR_ENDPOINT_URI}/login-success?i=${this.token}`, 237 | ); 238 | 239 | this.eventSource.onmessage = this.eventSourceMessage; 240 | } else { 241 | throw new Error("Error"); 242 | } 243 | } catch (err) { 244 | throw err; 245 | } 246 | } 247 | 248 | private async promptEmail() { 249 | const inputQuestion = { 250 | message: "Email", 251 | name: "email", 252 | type: "input", 253 | }; 254 | 255 | let email = ""; 256 | 257 | do { 258 | try { 259 | const input = await prompt(inputQuestion); 260 | const regex = /\S+@\S+\.\S+/; 261 | if (input.email.trim() === "" || !regex.test(input.email)) { 262 | Print.error("Enter a valid email. Press Ctrl-c or Esc key to exit."); 263 | } else { 264 | email = input.email; 265 | break; 266 | } 267 | } catch (err) { 268 | throw err; 269 | } 270 | } while (true); 271 | 272 | return email; 273 | } 274 | 275 | // protected pre() 276 | } 277 | -------------------------------------------------------------------------------- /src/subcommands/logout/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import fetch from "node-fetch"; 3 | import Auth from "../../Auth"; 4 | import CLI from "../../Cli"; 5 | import { EXIT_STATUS } from "../../constants"; 6 | import { KmdrAuthError } from "../../errors"; 7 | import Print from "../../Print"; 8 | 9 | export default class Logout extends CLI { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | public async init() { 15 | try { 16 | this.spinner?.start("Logging you out..."); 17 | const res = await fetch(`${this.KMDR_ENDPOINT_URI}/logout`, { 18 | headers: { 19 | Authorization: `Basic ${this.auth.token}`, 20 | "X-kmdr-origin": "cli", 21 | }, 22 | method: "POST", 23 | }); 24 | if (res.ok) { 25 | fs.unlinkSync(this.KMDR_AUTH_FILE); 26 | this.spinner?.succeed("You were logged out successfully!"); 27 | Print.newLine(); 28 | } else { 29 | throw new KmdrAuthError( 30 | `The session stored ${this.KMDR_AUTH_FILE} is invalid. Manually delete the file and log in again.`, 31 | ); 32 | } 33 | } catch (err) { 34 | if (err instanceof KmdrAuthError) { 35 | this.spinner?.fail(err.message); 36 | Print.error(""); 37 | Print.text(`$ rm ${this.KMDR_AUTH_FILE}`); 38 | Print.error(""); 39 | process.exit(EXIT_STATUS.USER_NOT_AUTHENTICATED); 40 | } else if (err.code === "ECONNREFUSED") { 41 | this.spinner?.fail("Could not reach the API registry. Are you connected to the internet?"); 42 | Print.error(""); 43 | Print.error(err); 44 | Print.error(""); 45 | process.exit(EXIT_STATUS.API_UNREACHABLE); 46 | } 47 | this.spinner?.fail(err); 48 | Print.error(""); 49 | process.exit(EXIT_STATUS.GENERIC); 50 | } 51 | } 52 | 53 | protected async hookBeforeLoadingAuth() { 54 | this.spinner?.start("Reading authentication file..."); 55 | 56 | if (!this.kmdrAuthFileExists) { 57 | this.spinner?.info("You're not logged in to kmdr. Why not log in? ;)"); 58 | Print.text(""); 59 | Print.text("$ kmdr login"); 60 | Print.text(""); 61 | process.exit(); 62 | } 63 | } 64 | 65 | protected async hookAfterLoadingAuth() { 66 | if (!Auth.isTokenValidFormat(this.auth.token)) { 67 | this.spinner?.fail( 68 | `File ${this.KMDR_AUTH_FILE} is invalid. Manually delete the file and log in again.`, 69 | ); 70 | Print.error(""); 71 | Print.text(`$ rm ${this.KMDR_AUTH_FILE}`); 72 | Print.error(""); 73 | process.exit(EXIT_STATUS.AUTH_FILE_INVALID); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/subcommands/settings/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { prompt } from "enquirer"; 3 | import CLI from "../../Cli"; 4 | import Print from "../../Print"; 5 | 6 | interface ThemeChoice { 7 | theme: string; 8 | } 9 | 10 | export default class Settings extends CLI { 11 | constructor() { 12 | super(); 13 | } 14 | 15 | public async init() { 16 | try { 17 | const theme = await this.promptThemeChoice(); 18 | this.spinner?.start("Saving to file..."); 19 | this.settingsManager.saveToDisk({ theme }); 20 | this.spinner?.succeed("Changes were saved!"); 21 | } catch (err) { 22 | if (err.code === "EACCES") { 23 | this.spinner?.fail( 24 | chalk.red( 25 | `Could not save changes. Validate that ${this.KMDR_SETTINGS_FILE} has Write permissions for current user`, 26 | ), 27 | ); 28 | Print.error(err); 29 | } 30 | } 31 | } 32 | 33 | private async promptThemeChoice() { 34 | const availableThemeChoices = []; 35 | 36 | for (const theme of this.settingsManager.availableThemes) { 37 | if (theme.name === this.settingsManager.theme.name) { 38 | availableThemeChoices.push({ 39 | message: `${theme.name} (${theme.mode} mode)`, 40 | value: theme.name, 41 | hint: "Current Theme", 42 | }); 43 | } else { 44 | availableThemeChoices.push({ 45 | message: `${theme.name} (${theme.mode} mode)`, 46 | value: theme.name, 47 | }); 48 | } 49 | } 50 | 51 | const themeChoices = { 52 | choices: [...availableThemeChoices], 53 | message: "Choose a theme", 54 | name: "theme", 55 | type: "select", 56 | }; 57 | 58 | try { 59 | const choice = await prompt(themeChoices); 60 | return choice.theme; 61 | } catch (err) { 62 | throw err; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/subcommands/version/index.ts: -------------------------------------------------------------------------------- 1 | import CLI from "../../Cli"; 2 | import Print from "../../Print"; 3 | 4 | export default class Version extends CLI { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | public async init() { 10 | Print.text(`You are using kmdr-cli version ${this.PKG_VERSION}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Kmdr.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExplainDev/kmdr-cli/0d801fe248ee3d5e8966d6dded488b1a21be29c7/tests/Kmdr.test.ts -------------------------------------------------------------------------------- /tests/SettingsManager.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExplainDev/kmdr-cli/0d801fe248ee3d5e8966d6dded488b1a21be29c7/tests/SettingsManager.test.ts -------------------------------------------------------------------------------- /tests/Theme.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExplainDev/kmdr-cli/0d801fe248ee3d5e8966d6dded488b1a21be29c7/tests/Theme.test.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | 4 | "compilerOptions": { 5 | /* Basic Options */ 6 | "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | "lib": [ 9 | "esnext", 10 | "esnext.array", 11 | "DOM" 12 | ] /* Specify library files to be included in the compilation. */, 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 18 | "sourceMap": true /* Generates corresponding '.map' file. */, 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist" /* Redirect output structure to the directory. */, 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true /* Enable all strict type-checking options. */, 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | "resolveJsonModule": true 66 | }, 67 | "exclude": ["node_modules", "dist", "coverage", "tests", "scripts"] 68 | } 69 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-standard", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": false, 5 | "no-console": false, 6 | "max-line-length": [true, 100] 7 | } 8 | } 9 | --------------------------------------------------------------------------------