custom-menu-cli 2.0.0 → 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,14 +88,41 @@ 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:
94
121
 
95
122
  ```json
96
123
  {
97
- "name": "Deploy Menu",
98
- "description": "Menu de navegação para deploys",
124
+ "name": "custom-menu-cli",
125
+ "description": "JSON-based terminal menu",
99
126
  "options": [
100
127
  {
101
128
  "id": "1",
@@ -114,6 +141,13 @@ O arquivo JSON que define o menu tem a seguinte estrutura:
114
141
  "name": "Up Service",
115
142
  "type": "action",
116
143
  "command": "echo 'Up A'"
144
+ },
145
+ {
146
+ "id": "1.3",
147
+ "name": "Restart Project A (from inside)",
148
+ "type": "custom-action",
149
+ "idList": ["1.1", "1.2"],
150
+ "confirm": true
117
151
  }
118
152
  ]
119
153
  },
@@ -123,6 +157,13 @@ O arquivo JSON que define o menu tem a seguinte estrutura:
123
157
  "type": "custom-action",
124
158
  "idList": ["1.1", "1.2"],
125
159
  "confirm": true
160
+ },
161
+ {
162
+ "id": "3",
163
+ "name": "Restart Project A (Nested)",
164
+ "type": "custom-action",
165
+ "idList": ["1.3"],
166
+ "confirm": true
126
167
  }
127
168
  ]
128
169
  }
package/README.md CHANGED
@@ -89,14 +89,41 @@ 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:
95
122
 
96
123
  ```json
97
124
  {
98
- "name": "Deploy Menu",
99
- "description": "Menu de navegação para deploys",
125
+ "name": "custom-menu-cli",
126
+ "description": "JSON-based terminal menu",
100
127
  "options": [
101
128
  {
102
129
  "id": "1",
@@ -115,6 +142,13 @@ The JSON file that defines the menu has the following structure:
115
142
  "name": "Up Service",
116
143
  "type": "action",
117
144
  "command": "echo 'Up A'"
145
+ },
146
+ {
147
+ "id": "1.3",
148
+ "name": "Restart Project A (from inside)",
149
+ "type": "custom-action",
150
+ "idList": ["1.1", "1.2"],
151
+ "confirm": true
118
152
  }
119
153
  ]
120
154
  },
@@ -124,6 +158,13 @@ The JSON file that defines the menu has the following structure:
124
158
  "type": "custom-action",
125
159
  "idList": ["1.1", "1.2"],
126
160
  "confirm": true
161
+ },
162
+ {
163
+ "id": "3",
164
+ "name": "Restart Project A (Nested)",
165
+ "type": "custom-action",
166
+ "idList": ["1.3"],
167
+ "confirm": true
127
168
  }
128
169
  ]
129
170
  }
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.0",
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
+ }