custom-menu-cli 1.0.0
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-en.md +89 -0
- package/README-pt.md +89 -0
- package/README.md +3 -0
- package/index.js +38 -0
- package/menu.json +33 -0
- package/package.json +21 -0
- package/src/actions.js +32 -0
- package/src/configLoader.js +48 -0
- package/src/menu.js +43 -0
- package/src/terminal.js +55 -0
package/README-en.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Custom Menu CLI
|
2
|
+
|
3
|
+
This is a command-line interface (CLI) tool that creates an interactive menu based on a JSON file. It's designed to simplify the execution of frequent commands in a terminal.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- Interactive menu in the terminal.
|
8
|
+
- Menu structure defined by a JSON file.
|
9
|
+
- Easy to configure and use.
|
10
|
+
- Support for command execution with confirmation.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
To install this tool globally, run the following command:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
npm install -g custom-menu-cli
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
To use the CLI, you can run the `custom-menu-cli` command, optionally passing the path to a JSON file. If no path is provided, it will look for a `menu.json` file in the current directory.
|
23
|
+
|
24
|
+
```bash
|
25
|
+
custom-menu-cli [path/to/your/menu.json]
|
26
|
+
```
|
27
|
+
|
28
|
+
## JSON Structure
|
29
|
+
|
30
|
+
The JSON file that defines the menu has the following structure:
|
31
|
+
|
32
|
+
```json
|
33
|
+
{
|
34
|
+
"name": "Deploy Menu",
|
35
|
+
"description": "Menu de navegação para deploys",
|
36
|
+
"options": [
|
37
|
+
{
|
38
|
+
"id": "1",
|
39
|
+
"name": "Projeto A",
|
40
|
+
"type": "navigation",
|
41
|
+
"options": [
|
42
|
+
{
|
43
|
+
"id": "1.1",
|
44
|
+
"name": "Down Service",
|
45
|
+
"type": "action",
|
46
|
+
"command": "echo 'Down A'",
|
47
|
+
"confirm": true
|
48
|
+
},
|
49
|
+
{
|
50
|
+
"id": "1.2",
|
51
|
+
"name": "Up Service",
|
52
|
+
"type": "action",
|
53
|
+
"command": "echo 'Up A'"
|
54
|
+
}
|
55
|
+
]
|
56
|
+
},
|
57
|
+
{
|
58
|
+
"id": "2",
|
59
|
+
"name": "Restart All",
|
60
|
+
"type": "custom-action",
|
61
|
+
"idList": ["1.1", "1.2"],
|
62
|
+
"confirm": true
|
63
|
+
}
|
64
|
+
]
|
65
|
+
}
|
66
|
+
```
|
67
|
+
|
68
|
+
### Fields
|
69
|
+
|
70
|
+
- `name`: The name of the menu.
|
71
|
+
- `description`: A brief description of the menu.
|
72
|
+
- `options`: An array of menu options.
|
73
|
+
- `id`: A unique identifier for the option.
|
74
|
+
- `name`: The text that will be displayed for the option.
|
75
|
+
- `type`: The type of option. It can be `action` (executes a command), `navigation` (opens a submenu) or `custom-action` (executes a list of commands from other actions).
|
76
|
+
- `command`: The command to be executed (if the type is `action`).
|
77
|
+
- `idList`: A list of ids from other actions to be executed (if the type is `custom-action`).
|
78
|
+
- `confirm`: A boolean that indicates whether a confirmation should be requested before executing the command.
|
79
|
+
- `options`: An array of sub-options (if the type is `navigation`).
|
80
|
+
|
81
|
+
## License
|
82
|
+
|
83
|
+
This project is licensed under the MIT License.
|
84
|
+
|
85
|
+
## Author
|
86
|
+
|
87
|
+
- **Mateus Medeiros**
|
88
|
+
- GitHub: [@mateusmed](https://github.com/mateusmed)
|
89
|
+
- LinkedIn: [Mateus Medeiros](https://www.linkedin.com/in/mateus-med/)
|
package/README-pt.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Custom Menu CLI
|
2
|
+
|
3
|
+
Esta é uma ferramenta de interface de linha de comando (CLI) que cria um menu interativo com base em um arquivo JSON. Ele foi projetado para simplificar a execução de comandos frequentes em um terminal.
|
4
|
+
|
5
|
+
## Funcionalidades
|
6
|
+
|
7
|
+
- Menu interativo no terminal.
|
8
|
+
- Estrutura do menu definida por um arquivo JSON.
|
9
|
+
- Fácil de configurar e usar.
|
10
|
+
- Suporte para execução de comandos com confirmação.
|
11
|
+
|
12
|
+
## Instalação
|
13
|
+
|
14
|
+
Para instalar esta ferramenta globalmente, execute o seguinte comando:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
npm install -g custom-menu-cli
|
18
|
+
```
|
19
|
+
|
20
|
+
## Uso
|
21
|
+
|
22
|
+
Para usar o CLI, você pode executar o comando `custom-menu-cli`, passando opcionalmente o caminho para um arquivo JSON. Se nenhum caminho for fornecido, ele procurará um arquivo `menu.json` no diretório atual.
|
23
|
+
|
24
|
+
```bash
|
25
|
+
custom-menu-cli [caminho/para/seu/menu.json]
|
26
|
+
```
|
27
|
+
|
28
|
+
## Estrutura do JSON
|
29
|
+
|
30
|
+
O arquivo JSON que define o menu tem a seguinte estrutura:
|
31
|
+
|
32
|
+
```json
|
33
|
+
{
|
34
|
+
"name": "Deploy Menu",
|
35
|
+
"description": "Menu de navegação para deploys",
|
36
|
+
"options": [
|
37
|
+
{
|
38
|
+
"id": "1",
|
39
|
+
"name": "Projeto A",
|
40
|
+
"type": "navigation",
|
41
|
+
"options": [
|
42
|
+
{
|
43
|
+
"id": "1.1",
|
44
|
+
"name": "Down Service",
|
45
|
+
"type": "action",
|
46
|
+
"command": "echo 'Down A'",
|
47
|
+
"confirm": true
|
48
|
+
},
|
49
|
+
{
|
50
|
+
"id": "1.2",
|
51
|
+
"name": "Up Service",
|
52
|
+
"type": "action",
|
53
|
+
"command": "echo 'Up A'"
|
54
|
+
}
|
55
|
+
]
|
56
|
+
},
|
57
|
+
{
|
58
|
+
"id": "2",
|
59
|
+
"name": "Restart All",
|
60
|
+
"type": "custom-action",
|
61
|
+
"idList": ["1.1", "1.2"],
|
62
|
+
"confirm": true
|
63
|
+
}
|
64
|
+
]
|
65
|
+
}
|
66
|
+
```
|
67
|
+
|
68
|
+
### Campos
|
69
|
+
|
70
|
+
- `name`: O nome do menu.
|
71
|
+
- `description`: Uma breve descrição do menu.
|
72
|
+
- `options`: Um array de opções do menu.
|
73
|
+
- `id`: Um identificador único para a opção.
|
74
|
+
- `name`: O texto que será exibido para a opção.
|
75
|
+
- `type`: O tipo de opção. Pode ser `action` (executa um comando), `navigation` (abre um submenu) ou `custom-action` (executa uma lista de comandos de outras ações).
|
76
|
+
- `command`: O comando a ser executado (se o tipo for `action`).
|
77
|
+
- `idList`: Uma lista de ids de outras ações a serem executados (se o tipo for `custom-action`).
|
78
|
+
- `confirm`: Um booleano que indica se uma confirmação deve ser solicitada antes de executar o comando.
|
79
|
+
- `options`: Um array de sub-opções (se o tipo for `navigation`).
|
80
|
+
|
81
|
+
## Licença
|
82
|
+
|
83
|
+
Este projeto está licenciado sob a Licença MIT.
|
84
|
+
|
85
|
+
## Autor
|
86
|
+
|
87
|
+
- **Mateus Medeiros**
|
88
|
+
- GitHub: [@mateusmed](https://github.com/mateusmed)
|
89
|
+
- LinkedIn: [Mateus Medeiros](https://www.linkedin.com/in/mateus-med/)
|
package/README.md
ADDED
package/index.js
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import chalk from 'chalk';
|
3
|
+
import { loadMenuConfig } from './src/configLoader.js';
|
4
|
+
import { showMenu, buildIdMap } from './src/menu.js';
|
5
|
+
|
6
|
+
(async () => {
|
7
|
+
const data = loadMenuConfig();
|
8
|
+
if (data.options) {
|
9
|
+
buildIdMap(data.options);
|
10
|
+
}
|
11
|
+
|
12
|
+
console.clear();
|
13
|
+
|
14
|
+
// Dynamic Header
|
15
|
+
const name = data.name || 'Custom Menu';
|
16
|
+
const description = data.description || 'A CLI Menu';
|
17
|
+
const lines = [name, description];
|
18
|
+
const maxLength = Math.max(...lines.map(line => line.length));
|
19
|
+
const boxWidth = maxLength + 4;
|
20
|
+
|
21
|
+
const topBorder = '╔' + '═'.repeat(boxWidth) + '╗';
|
22
|
+
const bottomBorder = '╚' + '═'.repeat(boxWidth) + '╝';
|
23
|
+
|
24
|
+
console.log(chalk.bold.blueBright(topBorder));
|
25
|
+
lines.forEach(line => {
|
26
|
+
const paddingTotal = boxWidth - line.length;
|
27
|
+
const paddingLeft = Math.floor(paddingTotal / 2);
|
28
|
+
const paddingRight = Math.ceil(paddingTotal / 2);
|
29
|
+
const paddedLine = `║${' '.repeat(paddingLeft)}${line}${' '.repeat(paddingRight)}║`;
|
30
|
+
console.log(chalk.bold.blueBright(paddedLine));
|
31
|
+
});
|
32
|
+
console.log(chalk.bold.blueBright(bottomBorder));
|
33
|
+
console.log(''); // For spacing
|
34
|
+
console.log(chalk.gray(`Developed by Mateus Medeiros - GitHub: @mateusmed`));
|
35
|
+
console.log(''); // For spacing
|
36
|
+
|
37
|
+
await showMenu(data);
|
38
|
+
})();
|
package/menu.json
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"name": "custom-menu-cli",
|
3
|
+
"description": "JSON-based terminal menu",
|
4
|
+
"options": [
|
5
|
+
{
|
6
|
+
"id": "1",
|
7
|
+
"name": "Projeto A",
|
8
|
+
"type": "navigation",
|
9
|
+
"options": [
|
10
|
+
{
|
11
|
+
"id": "1.1",
|
12
|
+
"name": "Down Service",
|
13
|
+
"type": "action",
|
14
|
+
"command": "echo 'Down A'",
|
15
|
+
"confirm": true
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"id": "1.2",
|
19
|
+
"name": "Up Service",
|
20
|
+
"type": "action",
|
21
|
+
"command": "echo 'Up A'"
|
22
|
+
}
|
23
|
+
]
|
24
|
+
},
|
25
|
+
{
|
26
|
+
"id": "2",
|
27
|
+
"name": "Restart All",
|
28
|
+
"type": "custom-action",
|
29
|
+
"idList": ["1.1", "1.2"],
|
30
|
+
"confirm": true
|
31
|
+
}
|
32
|
+
]
|
33
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"name": "custom-menu-cli",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Menu interativo baseado em JSON para execução de comandos no terminal",
|
5
|
+
"type": "module",
|
6
|
+
"main": "index.js",
|
7
|
+
"bin": {
|
8
|
+
"custom-menu-cli": "index.js"
|
9
|
+
},
|
10
|
+
"scripts": {
|
11
|
+
"start": "node index.js"
|
12
|
+
},
|
13
|
+
"keywords": ["cli", "menu", "json", "terminal", "deploy"],
|
14
|
+
"author": "Mateus Medeiros <https://github.com/mateusmed>",
|
15
|
+
"license": "MIT",
|
16
|
+
"dependencies": {
|
17
|
+
"chalk": "^5.3.0",
|
18
|
+
"dotenv": "^16.3.1",
|
19
|
+
"inquirer": "^9.2.16"
|
20
|
+
}
|
21
|
+
}
|
package/src/actions.js
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import inquirer from 'inquirer';
|
2
|
+
import chalk from 'chalk';
|
3
|
+
import {terminal} from './terminal.js';
|
4
|
+
|
5
|
+
async function confirmExecution(message) {
|
6
|
+
const {ok} = await inquirer.prompt({type: 'confirm', name: 'ok', message, default: false});
|
7
|
+
return ok;
|
8
|
+
}
|
9
|
+
|
10
|
+
export async function handleAction(selected) {
|
11
|
+
const proceed = selected.confirm ? await confirmExecution(chalk.yellow(`Executing command: [id: ${selected.id} name: ${selected.name} ]`)) : true;
|
12
|
+
if (proceed) {
|
13
|
+
await terminal.execCommandSync(selected.command);
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
export async function handleCustomAction(selected, flatMap) {
|
18
|
+
const proceed = selected.confirm ? await confirmExecution(chalk.yellow(`Execute command list ${selected.idList.join(', ')}?`)) : true;
|
19
|
+
if (proceed) {
|
20
|
+
for (const id of selected.idList) {
|
21
|
+
const cmd = flatMap[id];
|
22
|
+
if (cmd?.command) {
|
23
|
+
console.log(chalk.blue(`Executing command: [id: ${cmd.id} name: ${cmd.name} ]`));
|
24
|
+
await terminal.execCommandSync(cmd.command);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
export async function handleNavigation(selected) {
|
31
|
+
return selected;
|
32
|
+
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import chalk from 'chalk';
|
3
|
+
|
4
|
+
const defaultMenu = {
|
5
|
+
"name": "Example Menu",
|
6
|
+
"description": "This is a default example menu. Create a 'menu.json' to customize.",
|
7
|
+
"options": [
|
8
|
+
{
|
9
|
+
"id": "hello",
|
10
|
+
"name": "Say Hello",
|
11
|
+
"type": "action",
|
12
|
+
"command": "echo 'Hello, World!'",
|
13
|
+
"confirm": false
|
14
|
+
}
|
15
|
+
]
|
16
|
+
};
|
17
|
+
|
18
|
+
export function loadMenuConfig() {
|
19
|
+
const args = process.argv.slice(2);
|
20
|
+
const path = args[0];
|
21
|
+
let data;
|
22
|
+
|
23
|
+
if (path && fs.existsSync(path)) {
|
24
|
+
try {
|
25
|
+
data = JSON.parse(fs.readFileSync(path, 'utf-8'));
|
26
|
+
} catch (error) {
|
27
|
+
console.log(chalk.red(`Error parsing JSON file: ${path}`));
|
28
|
+
console.error(error);
|
29
|
+
process.exit(1);
|
30
|
+
}
|
31
|
+
} else if (path) {
|
32
|
+
console.log(chalk.red(`File not found: ${path}`));
|
33
|
+
process.exit(1);
|
34
|
+
} else if (fs.existsSync('./menu.json')) {
|
35
|
+
try {
|
36
|
+
data = JSON.parse(fs.readFileSync('./menu.json', 'utf-8'));
|
37
|
+
} catch (error) {
|
38
|
+
console.log(chalk.red(`Error parsing JSON file: ./menu.json`));
|
39
|
+
console.error(error);
|
40
|
+
process.exit(1);
|
41
|
+
}
|
42
|
+
} else {
|
43
|
+
console.log(chalk.yellow("No 'menu.json' found. Loading example menu."));
|
44
|
+
data = defaultMenu;
|
45
|
+
}
|
46
|
+
|
47
|
+
return data;
|
48
|
+
}
|
package/src/menu.js
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
import inquirer from 'inquirer';
|
2
|
+
import chalk from 'chalk';
|
3
|
+
import { handleAction, handleCustomAction, handleNavigation } from './actions.js';
|
4
|
+
|
5
|
+
export const flatMap = {};
|
6
|
+
|
7
|
+
export function buildIdMap(options) {
|
8
|
+
for (const opt of options) {
|
9
|
+
flatMap[opt.id] = opt;
|
10
|
+
if (opt.options) buildIdMap(opt.options);
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
export async function showMenu(menu) {
|
15
|
+
while (true) {
|
16
|
+
const choices = menu.options.map(o => `[${o.id}] ${o.name}`).concat([' Back']);
|
17
|
+
const { choice } = await inquirer.prompt({
|
18
|
+
type: 'list',
|
19
|
+
name: 'choice',
|
20
|
+
message: chalk.cyan(menu.name),
|
21
|
+
choices
|
22
|
+
});
|
23
|
+
|
24
|
+
if (choice === ' Back') return;
|
25
|
+
|
26
|
+
const selected = menu.options.find(o => `[${o.id}] ${o.name}` === choice);
|
27
|
+
if (!selected) continue;
|
28
|
+
|
29
|
+
switch (selected.type) {
|
30
|
+
case 'action':
|
31
|
+
await handleAction(selected);
|
32
|
+
break;
|
33
|
+
case 'custom-action':
|
34
|
+
await handleCustomAction(selected, flatMap);
|
35
|
+
break;
|
36
|
+
case 'navigation':
|
37
|
+
await showMenu(selected);
|
38
|
+
break;
|
39
|
+
default:
|
40
|
+
console.log(chalk.red(`Unknown action type: ${selected.type}`));
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
package/src/terminal.js
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
import { fileURLToPath } from 'url';
|
2
|
+
import { dirname, join } from 'path';
|
3
|
+
import { config } from 'dotenv';
|
4
|
+
import { access } from 'fs/promises';
|
5
|
+
import { execSync } from 'child_process';
|
6
|
+
import chalk from 'chalk';
|
7
|
+
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
9
|
+
const __dirname = dirname(__filename);
|
10
|
+
|
11
|
+
config({ path: join(__dirname, '../.env') });
|
12
|
+
|
13
|
+
export const terminal = {
|
14
|
+
|
15
|
+
async directoryExists(path) {
|
16
|
+
try {
|
17
|
+
await access(path);
|
18
|
+
return true;
|
19
|
+
} catch (error) {
|
20
|
+
console.log(chalk.red(`❌ Diretório não encontrado: ${path}`));
|
21
|
+
return false;
|
22
|
+
}
|
23
|
+
},
|
24
|
+
|
25
|
+
async execCommandSync(command) {
|
26
|
+
try {
|
27
|
+
console.log(chalk.yellow(`\n Execute command:\n\t${chalk.bgBlackBright.red(command)}\n`));
|
28
|
+
const output = execSync(command, { encoding: 'utf8' });
|
29
|
+
console.log(chalk.green(`✅ Command executed with success.\n`));
|
30
|
+
if (output.trim()) {
|
31
|
+
console.log('--------[output command]-------');
|
32
|
+
console.log(chalk.greenBright(output));
|
33
|
+
console.log('-------------------------------');
|
34
|
+
}
|
35
|
+
return output;
|
36
|
+
} catch (error) {
|
37
|
+
console.error('-------------------');
|
38
|
+
console.error(chalk.red(`Error on execute command:`));
|
39
|
+
console.error(chalk.red(error.message));
|
40
|
+
return `Error: ${error.message}`;
|
41
|
+
}
|
42
|
+
},
|
43
|
+
|
44
|
+
async execList(list) {
|
45
|
+
let output = '';
|
46
|
+
console.log(chalk.cyan(`\n Running list of command: (${list.length}):\n`));
|
47
|
+
for (let command of list) {
|
48
|
+
console.log(chalk.blue(`→ ${command}`));
|
49
|
+
const result = await this.execCommandSync(command);
|
50
|
+
output += result + '\n';
|
51
|
+
}
|
52
|
+
return output;
|
53
|
+
}
|
54
|
+
|
55
|
+
}
|