custom-menu-cli 2.0.1 → 3.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-pt.md CHANGED
@@ -88,6 +88,33 @@ async function iniciarMeuMenuCustomizado() {
88
88
  iniciarMeuMenuCustomizado();
89
89
  ```
90
90
 
91
+ ### 4. Geração de Menu Baseada em Pastas
92
+
93
+ O `custom-menu-cli` agora suporta a geração de menus a partir de uma pasta estruturada contendo arquivos JSON. Isso permite uma melhor organização e modularidade das suas definições de menu.
94
+
95
+ **Estrutura de Exemplo (`test_menus/`):**
96
+ ```
97
+ test_menus/
98
+ ├── 1-project-a/
99
+ │ ├── 1.1-down-service.json
100
+ │ ├── 1.2-up-service.json
101
+ │ └── 1.3-restart-project-a.json
102
+ ├── 2-restart-all.json
103
+ └── 3-restart-project-a-nested.json
104
+ ```
105
+
106
+ Cada arquivo `.json` dentro da pasta (e suas subpastas) representa uma opção de menu. Os diretórios são automaticamente convertidos em opções do tipo `navigation`.
107
+
108
+ **Como usar:**
109
+
110
+ Basta passar o caminho para a sua pasta de menu como argumento:
111
+
112
+ ```bash
113
+ custom-menu-cli ./caminho/para/sua/pasta_de_menu
114
+ ```
115
+
116
+ O CLI irá automaticamente descobrir e combinar todos os arquivos JSON válidos em uma única estrutura de menu.
117
+
91
118
  ## Estrutura do JSON
92
119
 
93
120
  O arquivo JSON que define o menu tem a seguinte estrutura:
package/README.md CHANGED
@@ -89,6 +89,33 @@ async function startMyCustomMenu() {
89
89
  startMyCustomMenu();
90
90
  ```
91
91
 
92
+ ### 4. Folder-based Menu Generation
93
+
94
+ The `custom-menu-cli` now supports generating menus from a structured folder containing JSON files. This allows for better organization and modularity of your menu definitions.
95
+
96
+ **Example Structure (`test_menus/`):**
97
+ ```
98
+ test_menus/
99
+ ├── 1-project-a/
100
+ │ ├── 1.1-down-service.json
101
+ │ ├── 1.2-up-service.json
102
+ │ └── 1.3-restart-project-a.json
103
+ ├── 2-restart-all.json
104
+ └── 3-restart-project-a-nested.json
105
+ ```
106
+
107
+ Each `.json` file within the folder (and its subfolders) represents a menu option. Directories are automatically converted into `navigation` type options.
108
+
109
+ **How to use:**
110
+
111
+ Simply pass the path to your menu folder as an argument:
112
+
113
+ ```bash
114
+ custom-menu-cli ./path/to/your/menu_folder
115
+ ```
116
+
117
+ The CLI will automatically discover and combine all valid JSON files into a single menu structure.
118
+
92
119
  ## JSON Structure
93
120
 
94
121
  The JSON file that defines the menu has the following structure:
package/index.js CHANGED
@@ -5,7 +5,7 @@ const { showMenu, buildIdMap } = require('./src/menu.js');
5
5
  const { displayHeader } = require('./src/header.js');
6
6
 
7
7
  async function runCli(menuPath = null) {
8
- const data = loadMenuConfig(menuPath);
8
+ const data = await loadMenuConfig(menuPath);
9
9
  if (data.options) {
10
10
  buildIdMap(data.options);
11
11
  }
@@ -18,7 +18,9 @@ async function runCli(menuPath = null) {
18
18
  }
19
19
 
20
20
  if (require.main === module) {
21
- runCli();
21
+ (async () => {
22
+ await runCli();
23
+ })();
22
24
  }
23
25
 
24
26
  module.exports = { runCli };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "custom-menu-cli",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "Menu interativo baseado em JSON para execução de comandos no terminal",
5
5
  "main": "index.js",
6
6
  "bin": "index.js",
@@ -1,5 +1,7 @@
1
- const fs = require('fs');
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
2
3
  const chalk = require('chalk');
4
+ const { validateMenuOption } = require('./menuValidator.js'); // Importar o validador
3
5
 
4
6
  const defaultMenu = {
5
7
  "name": "Example Menu",
@@ -15,7 +17,40 @@ const defaultMenu = {
15
17
  ]
16
18
  };
17
19
 
18
- function loadMenuConfig(menuPath = null) {
20
+ async function buildMenuOptions(dir) {
21
+ const entries = await fs.readdir(dir, { withFileTypes: true });
22
+ const options = [];
23
+
24
+ for (const entry of entries) {
25
+ const fullPath = path.join(dir, entry.name);
26
+ if (entry.isDirectory()) {
27
+ const subOptions = await buildMenuOptions(fullPath);
28
+ options.push({
29
+ id: entry.name,
30
+ name: `=> ${entry.name.toUpperCase()}`,
31
+ type: 'navigation',
32
+ options: subOptions
33
+ });
34
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
35
+ const fileContent = await fs.readFile(fullPath, 'utf-8');
36
+ let option;
37
+ try {
38
+ option = JSON.parse(fileContent);
39
+ } catch (parseError) {
40
+ console.error(chalk.red(`Erro: Arquivo JSON malformado em: ${fullPath}`));
41
+ console.error(chalk.red(`Detalhes: ${parseError.message}`));
42
+ process.exit(1);
43
+ }
44
+
45
+ validateMenuOption(option, fullPath); // Chamar o validador
46
+
47
+ options.push(option);
48
+ }
49
+ }
50
+ return options;
51
+ }
52
+
53
+ async function loadMenuConfig(menuPath = null) {
19
54
  let data;
20
55
  let finalPath = menuPath;
21
56
 
@@ -25,27 +60,35 @@ function loadMenuConfig(menuPath = null) {
25
60
  finalPath = args[0];
26
61
  }
27
62
 
28
- if (finalPath && fs.existsSync(finalPath)) {
63
+ if (finalPath) {
29
64
  try {
30
- data = JSON.parse(fs.readFileSync(finalPath, 'utf-8'));
65
+ const stats = await fs.stat(finalPath);
66
+ if (stats.isDirectory()) {
67
+ data = {
68
+ name: "Dynamic Menu",
69
+ description: `Menu generated from folder: ${finalPath}`,
70
+ options: await buildMenuOptions(finalPath)
71
+ };
72
+ } else { // It's a file
73
+ data = JSON.parse(await fs.readFile(finalPath, 'utf-8'));
74
+ validateMenuOption(data, finalPath); // Validar menu de arquivo único também
75
+ }
31
76
  } catch (error) {
32
- console.log(chalk.red(`Error parsing JSON file: ${finalPath}`));
77
+ console.log(chalk.red(`Erro ao processar o caminho: ${finalPath}`));
33
78
  console.error(error);
34
79
  process.exit(1);
35
80
  }
36
- } else if (finalPath) {
37
- console.log(chalk.red(`File not found: ${finalPath}`));
38
- process.exit(1);
39
- } else if (fs.existsSync('./menu.json')) {
81
+ } else if (fs.existsSync('./menu.json')) { // This part remains sync for now
40
82
  try {
41
83
  data = JSON.parse(fs.readFileSync('./menu.json', 'utf-8'));
84
+ validateMenuOption(data, './menu.json'); // Validar menu.json padrão
42
85
  } catch (error) {
43
- console.log(chalk.red(`Error parsing JSON file: ./menu.json`));
86
+ console.log(chalk.red(`Erro ao analisar o arquivo JSON: ./menu.json`));
44
87
  console.error(error);
45
88
  process.exit(1);
46
89
  }
47
90
  } else {
48
- console.log(chalk.yellow("No 'menu.json' found. Loading example menu."));
91
+ console.log(chalk.yellow("Nenhum 'menu.json' encontrado. Carregando menu de exemplo."));
49
92
  data = defaultMenu;
50
93
  }
51
94
 
@@ -0,0 +1,44 @@
1
+ const chalk = require('chalk');
2
+
3
+ function validateMenuOption(option, filePath) {
4
+ // --- Validation Logic ---
5
+ if (!option.id || typeof option.id !== 'string') {
6
+ console.error(chalk.red(`
7
+ Validation Error: 'id' missing or invalid in: ${filePath}`));
8
+ process.exit(1);
9
+ }
10
+ if (!option.name || typeof option.name !== 'string') {
11
+ console.error(chalk.red(`
12
+ Validation Error: 'name' missing or invalid in: ${filePath}`));
13
+ process.exit(1);
14
+ }
15
+ const validTypes = ['action', 'navigation', 'custom-action'];
16
+ if (!option.type || typeof option.type !== 'string' || !validTypes.includes(option.type)) {
17
+ console.error(chalk.red(`
18
+ Validation Error: 'type' missing or invalid (expected ${validTypes.join(', ')}) in: ${filePath}`));
19
+ process.exit(1);
20
+ }
21
+
22
+ if (option.type === 'action') {
23
+ if (!option.command || typeof option.command !== 'string') {
24
+ console.error(chalk.red(`
25
+ Validation Error: 'command' missing or invalid for type 'action' in: ${filePath}`));
26
+ process.exit(1);
27
+ }
28
+ } else if (option.type === 'custom-action') {
29
+ if (!Array.isArray(option.idList) || option.idList.length === 0) {
30
+ console.error(chalk.red(`
31
+ Validation Error: 'idList' missing or empty for type 'custom-action' in: ${filePath}`));
32
+ process.exit(1);
33
+ }
34
+ } else if (option.type === 'navigation') {
35
+ if (!Array.isArray(option.options) || option.options.length === 0) {
36
+ console.error(chalk.red(`
37
+ Validation Error: 'options' missing or empty for type 'navigation' in: ${filePath}. Navigation must come from directories or have sub-options.`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+ // --- End Validation Logic ---
42
+ }
43
+
44
+ module.exports = { validateMenuOption };
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "1.1",
3
+ "name": "Down Service",
4
+ "type": "action",
5
+ "command": "echo 'Down A'",
6
+ "confirm": true
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "1.2",
3
+ "name": "Up Service",
4
+ "type": "action",
5
+ "command": "echo 'Up A'"
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "1.3",
3
+ "name": "Restart Project A (from inside)",
4
+ "type": "custom-action",
5
+ "idList": ["1.1", "1.2"],
6
+ "confirm": true
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "2",
3
+ "name": "Restart All",
4
+ "type": "custom-action",
5
+ "idList": ["1.1", "1.2"],
6
+ "confirm": true
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "3",
3
+ "name": "Restart Project A (Nested)",
4
+ "type": "custom-action",
5
+ "idList": ["1.3"],
6
+ "confirm": true
7
+ }