mobbdev 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/index.mjs +14 -11
- package/package.json +1 -1
- package/src/commands/index.mjs +48 -34
- package/src/{feature → features}/analysis/github.mjs +3 -2
- package/src/{feature → features}/analysis/index.mjs +30 -32
- package/src/utils.mjs +12 -0
- package/src/yargs.mjs +9 -0
- /package/src/{feature → features}/analysis/callback-server.mjs +0 -0
- /package/src/{feature → features}/analysis/gql.mjs +0 -0
- /package/src/{feature → features}/analysis/prompts.mjs +0 -0
- /package/src/{feature → features}/analysis/snyk.mjs +0 -0
- /package/src/{feature → features}/analysis/upload-file.mjs +0 -0
package/README.md
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
# Bugsy
|
|
2
2
|
|
|
3
|
-
Bugsy is a command-line interface (CLI) tool that provides automatic security vulnerability remediation for your code. It is the community edition version of [Mobb](https://www.mobb.dev), the first vendor-agnostic
|
|
3
|
+
Bugsy is a command-line interface (CLI) tool that provides automatic security vulnerability remediation for your code. It is the community edition version of [Mobb](https://www.mobb.dev), the first vendor-agnostic automated security vulnerability remediation tool. Bugsy is designed to help developers quickly identify and fix security vulnerabilities in their code.
|
|
4
4
|
|
|
5
|
-
<img width="
|
|
5
|
+
<img width="1888" alt="Screenshot 2023-07-13 at 5 22 11 PM" src="https://github.com/mobb-dev/autofixer/assets/96389636/f1861bfe-c024-4976-aa57-8b6c1e2f4029">
|
|
6
6
|
|
|
7
7
|
## What is [Mobb](https://www.mobb.dev)?
|
|
8
8
|
|
|
9
|
-
[Mobb](https://www.mobb.dev) is the first vendor-agnostic automatic security vulnerability remediation tool. It ingests SAST results from Checkmarx, GitHub Advanced Security, and Snyk and produces code fixes for developers to review and commit to their code.
|
|
9
|
+
[Mobb](https://www.mobb.dev) is the first vendor-agnostic automatic security vulnerability remediation tool. It ingests SAST results from Checkmarx, CodeQL (GitHub Advanced Security), OpenText Fortify, and Snyk and produces code fixes for developers to review and commit to their code.
|
|
10
10
|
|
|
11
11
|
## What does Bugsy do?
|
|
12
12
|
|
|
13
|
+
Bugsy has two modes - Analyze (the user has a pre-generated SAST report from one of the supported SAST tools) and Scan (no SAST report needed).
|
|
14
|
+
|
|
15
|
+
Scan
|
|
16
|
+
|
|
13
17
|
- Uses Snyk CLI tool to run a SAST analysis on a given open-source GitHub repo
|
|
14
18
|
- Analyzes the vulnerability report to identify issues that can be remediated automatically
|
|
15
19
|
- Produces the code fixes and redirects the user to the fix report page on the Mobb platform
|
|
16
20
|
|
|
21
|
+
Analyze
|
|
22
|
+
|
|
23
|
+
- Analyzes the vulnerability report to identify issues that can be remediated automatically
|
|
24
|
+
- Produces the code fixes and redirects the user to the fix report page on the Mobb platform
|
|
25
|
+
|
|
17
26
|
## Disclaimer
|
|
18
27
|
|
|
19
|
-
This is a community edition version that only analyzes public GitHub repositories.
|
|
28
|
+
This is a community edition version that only analyzes public GitHub repositories. Analyzing private repositories is allowed for a limited amount of time.
|
|
20
29
|
Snyk CLI is used to produce a SAST vulnerability report.
|
|
21
30
|
|
|
22
|
-
- Only Java projects are supported at the moment.
|
|
23
|
-
- Only SQLi, CMDi, XSS, XXE, and Path Traversal are currently supported.
|
|
31
|
+
- Only Java and Node.js projects are supported at the moment.
|
|
24
32
|
|
|
25
33
|
## Usage
|
|
26
34
|
|
package/index.mjs
CHANGED
|
@@ -6,21 +6,24 @@ async function run() {
|
|
|
6
6
|
const args = await parseArgs(hideBin(process.argv));
|
|
7
7
|
const [command] = args._;
|
|
8
8
|
if (command === 'scan') {
|
|
9
|
-
const {
|
|
10
|
-
await scan(
|
|
11
|
-
{ repoUrl: repo, branch, scanner, apiKey },
|
|
12
|
-
{ skipPrompts: yes }
|
|
13
|
-
);
|
|
9
|
+
const { yes, ...restArgs } = args;
|
|
10
|
+
await scan(restArgs, { skipPrompts: yes });
|
|
14
11
|
}
|
|
15
12
|
if (command === 'analyze') {
|
|
16
|
-
const {
|
|
17
|
-
await analyze(
|
|
18
|
-
{ repoUrl: repo, scanFilePath: scanFile, branch, apiKey },
|
|
19
|
-
{ skipPrompts: yes }
|
|
20
|
-
);
|
|
13
|
+
const { yes, ...restArgs } = args;
|
|
14
|
+
await analyze(restArgs, { skipPrompts: yes });
|
|
21
15
|
}
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
(async () => {
|
|
25
|
-
|
|
19
|
+
try {
|
|
20
|
+
await run();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(
|
|
24
|
+
'Something went wrong, please try again or contact support if issue persists.'
|
|
25
|
+
);
|
|
26
|
+
console.error(err);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
26
29
|
})();
|
package/package.json
CHANGED
package/src/commands/index.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import chalkAnimation from 'chalk-animation';
|
|
5
|
-
import { choseScanner } from '../
|
|
5
|
+
import { choseScanner } from '../features/analysis/prompts.mjs';
|
|
6
6
|
import { SCANNERS, mobbAscii } from '../constants.mjs';
|
|
7
|
-
import { runAnalysis } from '../
|
|
7
|
+
import { runAnalysis } from '../features/analysis/index.mjs';
|
|
8
8
|
import { sleep } from '../utils.mjs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
|
|
@@ -13,65 +13,79 @@ const GITHUB_REPO_URL_PATTERN = new RegExp(
|
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
const UrlZ = z
|
|
16
|
-
.string({
|
|
16
|
+
.string({
|
|
17
|
+
invalid_type_error: 'is not a valid github URL',
|
|
18
|
+
})
|
|
17
19
|
.regex(GITHUB_REPO_URL_PATTERN, {
|
|
18
|
-
message: '
|
|
20
|
+
message: 'is not a valid github URL',
|
|
19
21
|
});
|
|
20
22
|
|
|
21
|
-
function
|
|
22
|
-
console.log('\n');
|
|
23
|
+
function printRepoUrlErrorMessage({ error, repoUrl, command }) {
|
|
23
24
|
const errorMessage = error.issues[error.issues.length - 1].message;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`Example: \n\tmobbdev ${command} ${chalk.bold(
|
|
29
|
-
'-r https://github.com/WebGoat/WebGoat'
|
|
25
|
+
console.error(`\nError: ${chalk.bold(repoUrl)} is ${errorMessage}`);
|
|
26
|
+
console.error(
|
|
27
|
+
`Example: \n\tmobbdev ${command} -r ${chalk.bold(
|
|
28
|
+
'https://github.com/WebGoat/WebGoat'
|
|
30
29
|
)}`
|
|
31
30
|
);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export async function analyze(
|
|
35
|
-
{
|
|
34
|
+
{ repo, scanFile, branch, apiKey, ci },
|
|
36
35
|
{ skipPrompts = false } = {}
|
|
37
36
|
) {
|
|
38
|
-
const { success, error } = UrlZ.safeParse(
|
|
37
|
+
const { success, error } = UrlZ.safeParse(repo);
|
|
39
38
|
if (!success) {
|
|
40
|
-
|
|
39
|
+
printRepoUrlErrorMessage({
|
|
40
|
+
error,
|
|
41
|
+
repoUrl: repo,
|
|
42
|
+
command: 'analyze',
|
|
43
|
+
});
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (ci && !apiKey) {
|
|
47
|
+
console.error(
|
|
48
|
+
'\nError: --ci flag requires --api-key to be provided as well'
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return;
|
|
52
|
+
if (!fs.existsSync(scanFile)) {
|
|
53
|
+
console.error(`\nCan't access ${chalk.bold(scanFile)}`);
|
|
54
|
+
process.exit(1);
|
|
46
55
|
}
|
|
47
|
-
if (path.extname(
|
|
48
|
-
console.
|
|
49
|
-
|
|
56
|
+
if (path.extname(scanFile) !== '.json') {
|
|
57
|
+
console.error(`\n${chalk.bold(scanFile)} is not a json file`);
|
|
58
|
+
process.exit(1);
|
|
50
59
|
}
|
|
51
|
-
await showWelcomeMessage(skipPrompts);
|
|
52
|
-
await runAnalysis(
|
|
53
|
-
{ repoUrl, scanFilePath, branch, apiKey },
|
|
54
|
-
{ skipPrompts }
|
|
55
|
-
);
|
|
60
|
+
!ci && (await showWelcomeMessage(skipPrompts));
|
|
61
|
+
await runAnalysis({ repo, scanFile, branch, apiKey, ci }, { skipPrompts });
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export async function scan(
|
|
59
|
-
{
|
|
65
|
+
{ repo, scanner, branch, apiKey, ci },
|
|
60
66
|
{ skipPrompts = false } = {}
|
|
61
67
|
) {
|
|
62
|
-
const { success, error } = UrlZ.safeParse(
|
|
68
|
+
const { success, error } = UrlZ.safeParse(repo);
|
|
69
|
+
if (ci && !apiKey) {
|
|
70
|
+
console.error(
|
|
71
|
+
'\nError: --ci flag requires --api-key to be provided as well'
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
if (!success) {
|
|
64
|
-
|
|
77
|
+
printRepoUrlErrorMessage({ error, repoUrl: repo, command: 'scan' });
|
|
78
|
+
process.exit(1);
|
|
65
79
|
}
|
|
66
|
-
await showWelcomeMessage(skipPrompts);
|
|
80
|
+
!ci && (await showWelcomeMessage(skipPrompts));
|
|
67
81
|
scanner ??= scanner || (await choseScanner());
|
|
68
82
|
if (scanner !== SCANNERS.Snyk) {
|
|
69
|
-
console.
|
|
83
|
+
console.error(
|
|
70
84
|
'Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon.'
|
|
71
85
|
);
|
|
72
|
-
|
|
86
|
+
process.exit(1);
|
|
73
87
|
}
|
|
74
|
-
await runAnalysis({
|
|
88
|
+
await runAnalysis({ repo, scanner, branch, apiKey }, { skipPrompts });
|
|
75
89
|
}
|
|
76
90
|
async function showWelcomeMessage(skipPrompts = false) {
|
|
77
91
|
console.log(mobbAscii);
|
|
@@ -6,7 +6,7 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import fetch from 'node-fetch';
|
|
7
7
|
import extract from 'extract-zip';
|
|
8
8
|
import Debug from 'debug';
|
|
9
|
-
import {
|
|
9
|
+
import { Spinner } from '../../utils.mjs';
|
|
10
10
|
import { GITHUB_CLIENT_ID, WEB_APP_URL } from '../../constants.mjs';
|
|
11
11
|
const pipeline = promisify(stream.pipeline);
|
|
12
12
|
const debug = Debug('mobbdev:github');
|
|
@@ -69,9 +69,10 @@ export async function getDefaultBranch(repoUrl, { token } = {}) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export async function downloadRepo(
|
|
72
|
-
{ repoUrl, reference, dirname },
|
|
72
|
+
{ repoUrl, reference, dirname, ci },
|
|
73
73
|
{ token } = {}
|
|
74
74
|
) {
|
|
75
|
+
const { createSpinner } = Spinner({ ci });
|
|
75
76
|
const repoSpinner = createSpinner('💾 Downloading Repo').start();
|
|
76
77
|
debug('download repo %s %s %s', repoUrl, reference, dirname);
|
|
77
78
|
const zipFilePath = path.join(dirname, 'repo.zip');
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import Configstore from 'configstore';
|
|
3
3
|
import Debug from 'debug';
|
|
4
|
-
import { createSpinner } from 'nanospinner';
|
|
5
4
|
import fs from 'node:fs';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -21,7 +20,7 @@ import { GQLClient } from './gql.mjs';
|
|
|
21
20
|
import { githubIntegrationPrompt, mobbAnalysisPrompt } from './prompts.mjs';
|
|
22
21
|
import { getSnykReport } from './snyk.mjs';
|
|
23
22
|
import { uploadFile } from './upload-file.mjs';
|
|
24
|
-
import { keypress } from '../../utils.mjs';
|
|
23
|
+
import { keypress, Spinner } from '../../utils.mjs';
|
|
25
24
|
|
|
26
25
|
const webLoginUrl = `${WEB_APP_URL}/cli-login`;
|
|
27
26
|
const githubSubmitUrl = `${WEB_APP_URL}/gh-callback`;
|
|
@@ -51,37 +50,34 @@ const config = new Configstore(packageJson.name, { token: '' });
|
|
|
51
50
|
debug('config %o', config);
|
|
52
51
|
|
|
53
52
|
export async function runAnalysis(
|
|
54
|
-
{
|
|
55
|
-
|
|
53
|
+
{ repo, scanner, scanFile, branch, apiKey, ci },
|
|
54
|
+
options
|
|
56
55
|
) {
|
|
57
56
|
try {
|
|
58
57
|
await _scan(
|
|
59
58
|
{
|
|
60
59
|
dirname: tmpObj.name,
|
|
61
|
-
|
|
60
|
+
repo,
|
|
62
61
|
scanner,
|
|
63
|
-
|
|
62
|
+
scanFile,
|
|
64
63
|
branch,
|
|
65
64
|
apiKey,
|
|
65
|
+
ci,
|
|
66
66
|
},
|
|
67
|
-
|
|
67
|
+
options
|
|
68
68
|
);
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.error(
|
|
71
|
-
'Something went wrong, please try again or contact support if issue persists.'
|
|
72
|
-
);
|
|
73
|
-
console.error(err);
|
|
74
69
|
} finally {
|
|
75
70
|
tmpObj.removeCallback();
|
|
76
71
|
}
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
export async function _scan(
|
|
80
|
-
{ dirname,
|
|
75
|
+
{ dirname, repo, scanFile, branch, apiKey, ci },
|
|
81
76
|
{ skipPrompts = false } = {}
|
|
82
77
|
) {
|
|
83
|
-
debug('start %s %s', dirname,
|
|
84
|
-
|
|
78
|
+
debug('start %s %s', dirname, repo);
|
|
79
|
+
const { createSpinner } = Spinner({ ci });
|
|
80
|
+
skipPrompts = skipPrompts || ci;
|
|
85
81
|
let token = config.get('token');
|
|
86
82
|
debug('token %s', token);
|
|
87
83
|
apiKey ?? debug('token %s', apiKey);
|
|
@@ -89,7 +85,7 @@ export async function _scan(
|
|
|
89
85
|
await handleMobbLogin();
|
|
90
86
|
const userInfo = await gqlClient.getUserInfo();
|
|
91
87
|
let { githubToken } = userInfo;
|
|
92
|
-
const isRepoAvailable = await canReachRepo(
|
|
88
|
+
const isRepoAvailable = await canReachRepo(repo, {
|
|
93
89
|
token: userInfo.githubToken,
|
|
94
90
|
});
|
|
95
91
|
if (!isRepoAvailable) {
|
|
@@ -98,7 +94,7 @@ export async function _scan(
|
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
const reference =
|
|
101
|
-
branch ?? (await getDefaultBranch(
|
|
97
|
+
branch ?? (await getDefaultBranch(repo, { token: githubToken }));
|
|
102
98
|
const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
|
|
103
99
|
debug('org id %s', organizationId);
|
|
104
100
|
debug('project id %s', projectId);
|
|
@@ -107,14 +103,15 @@ export async function _scan(
|
|
|
107
103
|
|
|
108
104
|
const repositoryRoot = await downloadRepo(
|
|
109
105
|
{
|
|
110
|
-
repoUrl,
|
|
106
|
+
repoUrl: repo,
|
|
111
107
|
reference,
|
|
112
108
|
dirname,
|
|
109
|
+
ci,
|
|
113
110
|
},
|
|
114
111
|
{ token: githubToken }
|
|
115
112
|
);
|
|
116
113
|
|
|
117
|
-
const reportPath =
|
|
114
|
+
const reportPath = scanFile || (await getReportFromSnyk());
|
|
118
115
|
|
|
119
116
|
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
120
117
|
await uploadFile(
|
|
@@ -127,7 +124,7 @@ export async function _scan(
|
|
|
127
124
|
try {
|
|
128
125
|
await gqlClient.submitVulnerabilityReport(
|
|
129
126
|
uploadData.fixReportId,
|
|
130
|
-
|
|
127
|
+
repo,
|
|
131
128
|
reference,
|
|
132
129
|
projectId
|
|
133
130
|
);
|
|
@@ -139,7 +136,7 @@ export async function _scan(
|
|
|
139
136
|
debug('report %o', report);
|
|
140
137
|
|
|
141
138
|
const results = ((report.runs || [])[0] || {}).results || [];
|
|
142
|
-
if (results.length === 0 && !
|
|
139
|
+
if (results.length === 0 && !scanFile) {
|
|
143
140
|
mobbSpinner.success({
|
|
144
141
|
text: '🕵️♂️ Report did not detect any vulnerabilities — nothing to fix.',
|
|
145
142
|
});
|
|
@@ -150,7 +147,6 @@ export async function _scan(
|
|
|
150
147
|
|
|
151
148
|
await askToOpenAnalysis();
|
|
152
149
|
}
|
|
153
|
-
process.exit(0);
|
|
154
150
|
async function getReportFromSnyk() {
|
|
155
151
|
const reportPath = path.join(dirname, 'report.json');
|
|
156
152
|
|
|
@@ -163,15 +159,18 @@ export async function _scan(
|
|
|
163
159
|
return reportPath;
|
|
164
160
|
}
|
|
165
161
|
async function askToOpenAnalysis() {
|
|
162
|
+
const reportUrl = `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`;
|
|
163
|
+
!ci && console.log('You can access the report at: \n');
|
|
164
|
+
console.log(reportUrl);
|
|
166
165
|
!skipPrompts && (await mobbAnalysisPrompt());
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
|
|
167
|
+
!ci && open(reportUrl);
|
|
168
|
+
!ci &&
|
|
169
|
+
console.log(
|
|
170
|
+
chalk.bgBlue(
|
|
171
|
+
'\n\n My work here is done for now, see you soon! 🕵️♂️ '
|
|
172
|
+
)
|
|
173
|
+
);
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
async function handleMobbLogin() {
|
|
@@ -190,7 +189,6 @@ export async function _scan(
|
|
|
190
189
|
text: '🔓 Logged in to Mobb failed - check your api-key',
|
|
191
190
|
});
|
|
192
191
|
process.exit(1);
|
|
193
|
-
return;
|
|
194
192
|
}
|
|
195
193
|
const mobbLoginSpinner = createSpinner().start();
|
|
196
194
|
if (!skipPrompts) {
|
|
@@ -214,7 +212,7 @@ export async function _scan(
|
|
|
214
212
|
mobbLoginSpinner.error({
|
|
215
213
|
text: 'Something went wrong, API token is invalid.',
|
|
216
214
|
});
|
|
217
|
-
process.exit(
|
|
215
|
+
process.exit(1);
|
|
218
216
|
}
|
|
219
217
|
debug('set token %s', token);
|
|
220
218
|
config.set('token', token);
|
package/src/utils.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
|
+
import { createSpinner as _createSpinner } from 'nanospinner';
|
|
3
|
+
import { PassThrough } from 'stream';
|
|
2
4
|
export const sleep = (ms = 2000) => new Promise((r) => setTimeout(r, ms));
|
|
3
5
|
|
|
4
6
|
export async function keypress() {
|
|
@@ -16,3 +18,13 @@ export async function keypress() {
|
|
|
16
18
|
});
|
|
17
19
|
});
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
export function Spinner({ ci = false } = {}) {
|
|
23
|
+
return {
|
|
24
|
+
createSpinner: (text, options) =>
|
|
25
|
+
_createSpinner(text, {
|
|
26
|
+
stream: ci ? new PassThrough() : undefined,
|
|
27
|
+
...options,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
package/src/yargs.mjs
CHANGED
|
@@ -24,6 +24,13 @@ const apiKeyOption = {
|
|
|
24
24
|
describe: chalk.bold('Mobb authentication api-key'),
|
|
25
25
|
type: 'string',
|
|
26
26
|
};
|
|
27
|
+
const ciOption = {
|
|
28
|
+
describe: chalk.bold(
|
|
29
|
+
'Run in CI mode, prompts and browser will not be opened'
|
|
30
|
+
),
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: false,
|
|
33
|
+
};
|
|
27
34
|
|
|
28
35
|
export const parseArgs = (args) => {
|
|
29
36
|
const yargsInstance = yargs(args);
|
|
@@ -81,6 +88,7 @@ export const parseArgs = (args) => {
|
|
|
81
88
|
b: branchOption,
|
|
82
89
|
y: yesOption,
|
|
83
90
|
['api-key']: apiKeyOption,
|
|
91
|
+
ci: ciOption,
|
|
84
92
|
});
|
|
85
93
|
},
|
|
86
94
|
})
|
|
@@ -91,6 +99,7 @@ export const parseArgs = (args) => {
|
|
|
91
99
|
yargsInstance.showHelp();
|
|
92
100
|
},
|
|
93
101
|
})
|
|
102
|
+
.strictOptions()
|
|
94
103
|
.help('h')
|
|
95
104
|
.alias('h', 'help')
|
|
96
105
|
.epilog(chalk.bgBlue('Made with ❤️ by Mobb'))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|