├── .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 
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 |
--------------------------------------------------------------------------------