dex-termux-cli 0.1.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.md +89 -0
- package/bin/Dex +7 -0
- package/bin/dex +7 -0
- package/data/commands/explain.json +74 -0
- package/icon/banner.png +0 -0
- package/icon/icon-dex.png +0 -0
- package/icon/logo-transparent.png +0 -0
- package/icon/logo.png +0 -0
- package/package.json +40 -0
- package/src/app/main.js +65 -0
- package/src/commands/android-shell.js +53 -0
- package/src/commands/explain.js +25 -0
- package/src/commands/install.js +1013 -0
- package/src/commands/menu.js +120 -0
- package/src/commands/safe-shell.js +200 -0
- package/src/commands/search.js +41 -0
- package/src/commands/tree.js +61 -0
- package/src/core/args.js +114 -0
- package/src/core/config.js +122 -0
- package/src/core/scopes.js +31 -0
- package/src/ui/output.js +129 -0
- package/src/ui/prompt.js +299 -0
- package/src/utils/fs-search.js +63 -0
- package/src/utils/fs-tree.js +131 -0
- package/src/utils/project-context.js +82 -0
package/src/ui/output.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getUserConfigPath } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export function printHelp() {
|
|
5
|
+
console.log('Dex CLI');
|
|
6
|
+
console.log('');
|
|
7
|
+
console.log('Acciones principales:');
|
|
8
|
+
console.log(' dex -h ver ayuda');
|
|
9
|
+
console.log(' dex -m abrir menu');
|
|
10
|
+
console.log(' dex -e ls explicar un comando');
|
|
11
|
+
console.log(' dex -b "archivo" buscar por nombre');
|
|
12
|
+
console.log(' dex -t src ver arbol guiado');
|
|
13
|
+
console.log(' dex -a abrir Android directo');
|
|
14
|
+
console.log(' dex -i instalar dependencias del proyecto');
|
|
15
|
+
console.log(' dex -r abrir shell del modo seguro');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log('Busqueda:');
|
|
18
|
+
console.log(' dex -b "foto"');
|
|
19
|
+
console.log(' dex -b "*.js" --scope actual');
|
|
20
|
+
console.log(' Dex -b "whatsapp" --scope android');
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log('Arbol:');
|
|
23
|
+
console.log(' dex -t');
|
|
24
|
+
console.log(' dex -t src --scope actual');
|
|
25
|
+
console.log(' dex -t . --depth 3');
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log('Android:');
|
|
28
|
+
console.log(' dex -a');
|
|
29
|
+
console.log(' activar en: ' + getUserConfigPath());
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('Instalacion segura de proyectos:');
|
|
32
|
+
console.log(' dex -i');
|
|
33
|
+
console.log(' detecta el lenguaje del proyecto actual');
|
|
34
|
+
console.log(' intenta instalar dependencias segun el lenguaje');
|
|
35
|
+
console.log(' si falta el runtime intenta traerlo con pkg');
|
|
36
|
+
console.log(' en Android storage ya existe modo seguro para Python, Node y PHP');
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log('Modo seguro:');
|
|
39
|
+
console.log(' dex -r');
|
|
40
|
+
console.log(' abre una shell del entorno seguro del proyecto actual');
|
|
41
|
+
console.log(' en Python usa el venv seguro; en Node y PHP abre la copia segura en HOME');
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log('Extras visuales:');
|
|
44
|
+
console.log(' detector de proyecto por carpeta actual');
|
|
45
|
+
console.log(' badge de lenguaje con color');
|
|
46
|
+
console.log(' activar en Ajustes y funciones');
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log('Scopes:');
|
|
49
|
+
console.log(' actual carpeta donde estas parado');
|
|
50
|
+
console.log(' home tu entorno de Termux');
|
|
51
|
+
console.log(' android /sdcard y almacenamiento accesible');
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('Atajos:');
|
|
54
|
+
console.log(' -h, --help');
|
|
55
|
+
console.log(' -m, --menu');
|
|
56
|
+
console.log(' -e, --explicar');
|
|
57
|
+
console.log(' -b, --buscar');
|
|
58
|
+
console.log(' -t, --tree');
|
|
59
|
+
console.log(' -a, --android');
|
|
60
|
+
console.log(' -i, --instalar');
|
|
61
|
+
console.log(' -r, --seguro');
|
|
62
|
+
console.log(' -s, --scope');
|
|
63
|
+
console.log(' -d, --depth');
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log('Nota: si no pasas --scope en busqueda o tree, Dex te pregunta antes.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function printSection(title) {
|
|
69
|
+
console.log('== ' + title + ' ==');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function printProjectContextLine(line) {
|
|
73
|
+
console.log(line);
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatSearchResults(results, root) {
|
|
78
|
+
if (!results.length) {
|
|
79
|
+
return 'No se encontraron coincidencias.';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results
|
|
83
|
+
.map((item, index) => {
|
|
84
|
+
const relative = path.relative(root, item.path) || '.';
|
|
85
|
+
const suffix = item.type === 'directory' ? '/' : '';
|
|
86
|
+
return (index + 1) + '. ' + relative + suffix;
|
|
87
|
+
})
|
|
88
|
+
.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function printExplainEntry(topic, entry) {
|
|
92
|
+
const notes = entry.notes.map((note, index) => (index + 1) + '. ' + note).join('\n');
|
|
93
|
+
|
|
94
|
+
return [
|
|
95
|
+
'Comando : ' + topic,
|
|
96
|
+
'Que hace: ' + entry.summary,
|
|
97
|
+
'Riesgo : ' + entry.risk,
|
|
98
|
+
'',
|
|
99
|
+
'Uso base:',
|
|
100
|
+
entry.usage,
|
|
101
|
+
'',
|
|
102
|
+
'Notas:',
|
|
103
|
+
notes,
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatTreeReport(report) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
|
|
110
|
+
lines.push('Punto : ' + report.label);
|
|
111
|
+
lines.push('Ruta : ' + report.root);
|
|
112
|
+
lines.push('Prof. : ' + report.depth);
|
|
113
|
+
lines.push('Modo : vista guiada de carpetas y archivos');
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('Mapa:');
|
|
116
|
+
lines.push(...report.lines);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('Resumen: ' + report.directories + ' carpetas, ' + report.files + ' archivos visibles.');
|
|
119
|
+
|
|
120
|
+
if (report.truncatedByDepth) {
|
|
121
|
+
lines.push('Nota : hay mas niveles abajo. Prueba con --depth ' + (report.depth + 1) + '.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (report.truncatedByLimit) {
|
|
125
|
+
lines.push('Nota : la vista fue recortada para que no se vuelva ruido.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return lines.join('\n');
|
|
129
|
+
}
|
package/src/ui/prompt.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { moveCursor, cursorTo } from 'node:readline';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
|
|
5
|
+
const ANSI = {
|
|
6
|
+
clearDown: '\x1B[0J',
|
|
7
|
+
green: '\x1B[32m',
|
|
8
|
+
red: '\x1B[31m',
|
|
9
|
+
reset: '\x1B[0m',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function chooseSearchScope(options) {
|
|
13
|
+
return chooseNumericOption({
|
|
14
|
+
title: 'Elige donde quieres buscar:',
|
|
15
|
+
options,
|
|
16
|
+
getValue: (option) => option.key,
|
|
17
|
+
getLines: (option) => [option.label, option.description, option.root],
|
|
18
|
+
prompt: 'Scope [1-' + options.length + ', Enter=1]: ',
|
|
19
|
+
errorMessage: 'Opcion no valida. Usa un numero o actual/home/android.',
|
|
20
|
+
directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function chooseExplainTopic(entries) {
|
|
25
|
+
const topics = Object.keys(entries).map((topic) => ({ topic }));
|
|
26
|
+
|
|
27
|
+
return chooseNumericOption({
|
|
28
|
+
title: 'Elige un comando para explicar:',
|
|
29
|
+
options: topics,
|
|
30
|
+
getValue: (option) => option.topic,
|
|
31
|
+
getLines: (option) => [option.topic],
|
|
32
|
+
prompt: 'Tema [1-' + topics.length + ', nombre, Enter=1]: ',
|
|
33
|
+
errorMessage: 'Opcion no valida. Usa un numero o el nombre del comando.',
|
|
34
|
+
directMatch: (answer) => topics.find((option) => option.topic === answer.toLowerCase())?.topic,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function chooseMenuAction() {
|
|
39
|
+
const options = [
|
|
40
|
+
{ key: 'search', label: 'Buscar archivos o carpetas' },
|
|
41
|
+
{ key: 'explain', label: 'Explicar un comando' },
|
|
42
|
+
{ key: 'tree', label: 'Ver arbol de una ruta' },
|
|
43
|
+
{ key: 'safe-shell', label: 'Entrar al modo seguro del proyecto' },
|
|
44
|
+
{ key: 'settings', label: 'Ajustes y funciones' },
|
|
45
|
+
{ key: 'help', label: 'Ver ayuda' },
|
|
46
|
+
{ key: 'exit', label: 'Salir' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return chooseNumericOption({
|
|
50
|
+
title: 'Dex Menu',
|
|
51
|
+
options,
|
|
52
|
+
getValue: (option) => option.key,
|
|
53
|
+
getLines: (option) => [option.label],
|
|
54
|
+
prompt: 'Opcion [1-' + options.length + ', Enter=1]: ',
|
|
55
|
+
errorMessage: 'Opcion no valida. Usa un numero entre 1 y 7.',
|
|
56
|
+
directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function chooseSettingsAction(config) {
|
|
61
|
+
const options = [
|
|
62
|
+
{
|
|
63
|
+
key: 'toggle-android-shortcut',
|
|
64
|
+
label: 'Acceso rapido a Android',
|
|
65
|
+
status: formatStatus(config.features.androidShortcut),
|
|
66
|
+
usage: 'permite dex -a',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: 'toggle-project-badge',
|
|
70
|
+
label: 'Detector de proyecto y color',
|
|
71
|
+
status: formatStatus(config.features.projectBadge),
|
|
72
|
+
usage: 'muestra lenguaje y color del proyecto',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'toggle-smart-project-install',
|
|
76
|
+
label: 'Instalacion segura de proyectos',
|
|
77
|
+
status: formatStatus(config.features.smartProjectInstall),
|
|
78
|
+
usage: 'rescata instalaciones y puede instalar runtimes con pkg',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'back',
|
|
82
|
+
label: 'Volver al menu principal',
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return chooseNumericOption({
|
|
87
|
+
title: 'Ajustes de Dex',
|
|
88
|
+
options,
|
|
89
|
+
getValue: (option) => option.key,
|
|
90
|
+
getLines: (option) => {
|
|
91
|
+
if (option.key === 'back') {
|
|
92
|
+
return [option.label];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
option.label,
|
|
97
|
+
'Estado : ' + option.status,
|
|
98
|
+
'Uso : ' + option.usage,
|
|
99
|
+
];
|
|
100
|
+
},
|
|
101
|
+
prompt: 'Ajuste [1-' + options.length + ', Enter=1]: ',
|
|
102
|
+
errorMessage: 'Opcion no valida. Usa 1, 2, 3 o 4.',
|
|
103
|
+
directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
|
|
104
|
+
style: 'card',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function chooseFeatureToggle(currentValue, featureName, description) {
|
|
109
|
+
const options = [
|
|
110
|
+
{
|
|
111
|
+
key: 'enable',
|
|
112
|
+
label: 'Activar',
|
|
113
|
+
usage: description,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: 'disable',
|
|
117
|
+
label: 'Desactivar',
|
|
118
|
+
usage: 'oculta ' + featureName.toLowerCase() + ' hasta volver a activarlo',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: 'back',
|
|
122
|
+
label: 'Volver',
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
return chooseNumericOption({
|
|
127
|
+
title: featureName,
|
|
128
|
+
options,
|
|
129
|
+
getValue: (option) => option.key,
|
|
130
|
+
getLines: (option) => {
|
|
131
|
+
if (option.key === 'back') {
|
|
132
|
+
return [option.label];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return [
|
|
136
|
+
option.label,
|
|
137
|
+
'Uso : ' + option.usage,
|
|
138
|
+
];
|
|
139
|
+
},
|
|
140
|
+
prompt: 'Accion [1-' + options.length + ', Enter=3]: ',
|
|
141
|
+
errorMessage: 'Opcion no valida. Usa 1, 2 o 3.',
|
|
142
|
+
directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
|
|
143
|
+
defaultIndex: 2,
|
|
144
|
+
style: 'card',
|
|
145
|
+
introLines: ['Estado actual: ' + formatStatus(currentValue)],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function chooseSearchPatternFromMenu() {
|
|
150
|
+
const rl = readline.createInterface({ input, output });
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
while (true) {
|
|
154
|
+
const answer = (await rl.question('Patron a buscar: ')).trim();
|
|
155
|
+
|
|
156
|
+
if (answer) {
|
|
157
|
+
return answer;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('Debes escribir algo para buscar.');
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
rl.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function chooseTreeTargetFromMenu() {
|
|
168
|
+
const rl = readline.createInterface({ input, output });
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const answer = (await rl.question('Ruta a mostrar [Enter=.]: ')).trim();
|
|
172
|
+
return answer || '.';
|
|
173
|
+
} finally {
|
|
174
|
+
rl.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function chooseNumericOption({
|
|
179
|
+
title,
|
|
180
|
+
options,
|
|
181
|
+
getValue,
|
|
182
|
+
getLines,
|
|
183
|
+
prompt,
|
|
184
|
+
errorMessage,
|
|
185
|
+
directMatch,
|
|
186
|
+
defaultIndex = 0,
|
|
187
|
+
style = 'simple',
|
|
188
|
+
introLines = [],
|
|
189
|
+
}) {
|
|
190
|
+
const rl = readline.createInterface({ input, output });
|
|
191
|
+
const blockLineCount = countRenderedLines(options, getLines, introLines, style) + 5;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
printOptionBlock(title, options, getLines, style, introLines);
|
|
195
|
+
|
|
196
|
+
while (true) {
|
|
197
|
+
const answer = (await rl.question(prompt)).trim();
|
|
198
|
+
const selectedValue = resolveAnswer(answer, options, getValue, directMatch, defaultIndex);
|
|
199
|
+
|
|
200
|
+
if (selectedValue) {
|
|
201
|
+
clearOptionBlock(blockLineCount);
|
|
202
|
+
return selectedValue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(errorMessage);
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
rl.close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printOptionBlock(title, options, getLines, style, introLines) {
|
|
213
|
+
console.log(title);
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
for (const line of introLines) {
|
|
217
|
+
console.log(line);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (introLines.length) {
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
options.forEach((option, index) => {
|
|
225
|
+
const lines = getLines(option);
|
|
226
|
+
|
|
227
|
+
if (style === 'card') {
|
|
228
|
+
console.log('[' + (index + 1) + '] ' + lines[0]);
|
|
229
|
+
|
|
230
|
+
for (let lineIndex = 1; lineIndex < lines.length; lineIndex += 1) {
|
|
231
|
+
console.log(' - ' + lines[lineIndex]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log('');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log((index + 1) + '. ' + lines[0]);
|
|
239
|
+
|
|
240
|
+
for (let lineIndex = 1; lineIndex < lines.length; lineIndex += 1) {
|
|
241
|
+
console.log(' - ' + lines[lineIndex]);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
console.log('Tip: puedes escribir el numero o el nombre corto cuando aplique.');
|
|
246
|
+
console.log('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function countRenderedLines(options, getLines, introLines, style) {
|
|
250
|
+
let count = 2 + introLines.length;
|
|
251
|
+
|
|
252
|
+
if (introLines.length) {
|
|
253
|
+
count += 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const option of options) {
|
|
257
|
+
count += getLines(option).length;
|
|
258
|
+
|
|
259
|
+
if (style === 'card') {
|
|
260
|
+
count += 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return count;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveAnswer(answer, options, getValue, directMatch, defaultIndex) {
|
|
268
|
+
if (!answer) {
|
|
269
|
+
return getValue(options[defaultIndex]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const numeric = Number.parseInt(answer, 10);
|
|
273
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= options.length) {
|
|
274
|
+
return getValue(options[numeric - 1]);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return directMatch(answer) || '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function clearOptionBlock(lineCount) {
|
|
281
|
+
if (!input.isTTY || !output.isTTY) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
moveCursor(output, 0, -lineCount);
|
|
286
|
+
cursorTo(output, 0);
|
|
287
|
+
output.write(ANSI.clearDown);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function formatStatus(enabled) {
|
|
291
|
+
const text = enabled ? 'activado' : 'desactivado';
|
|
292
|
+
|
|
293
|
+
if (!output.isTTY) {
|
|
294
|
+
return text;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const color = enabled ? ANSI.green : ANSI.red;
|
|
298
|
+
return color + text + ANSI.reset;
|
|
299
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function searchEntries({ root, pattern, limit = 80 }) {
|
|
5
|
+
const results = [];
|
|
6
|
+
const matcher = createMatcher(pattern);
|
|
7
|
+
|
|
8
|
+
async function walk(currentPath) {
|
|
9
|
+
if (results.length >= limit) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (results.length >= limit) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
26
|
+
const relativePath = path.relative(root, fullPath);
|
|
27
|
+
|
|
28
|
+
if (matcher(entry.name) || matcher(relativePath)) {
|
|
29
|
+
results.push({
|
|
30
|
+
path: fullPath,
|
|
31
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry.isDirectory() && !shouldSkipDirectory(entry.name)) {
|
|
36
|
+
await walk(fullPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await walk(root);
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createMatcher(pattern) {
|
|
46
|
+
const clean = pattern.trim().toLowerCase();
|
|
47
|
+
|
|
48
|
+
if (clean.includes('*')) {
|
|
49
|
+
const source = escapeRegex(clean).replaceAll('\\*', '.*');
|
|
50
|
+
const regex = new RegExp(`^${source}$`, 'i');
|
|
51
|
+
return (value) => regex.test(value.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (value) => value.toLowerCase().includes(clean);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function escapeRegex(value) {
|
|
58
|
+
return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shouldSkipDirectory(name) {
|
|
62
|
+
return name === '.git' || name === 'node_modules';
|
|
63
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const COLORS = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
folder: '\x1b[36m',
|
|
7
|
+
hint: '\x1b[90m',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function buildTreeReport({ root, label, depth = 2, limit = 120 }) {
|
|
11
|
+
const lines = [formatDirectoryLabel(`${path.basename(root) || root}/`)];
|
|
12
|
+
const counters = {
|
|
13
|
+
directories: 0,
|
|
14
|
+
files: 0,
|
|
15
|
+
visibleNodes: 0,
|
|
16
|
+
truncatedByDepth: false,
|
|
17
|
+
truncatedByLimit: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
await walk(root, '', 1);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
label,
|
|
24
|
+
root,
|
|
25
|
+
depth,
|
|
26
|
+
lines,
|
|
27
|
+
directories: counters.directories,
|
|
28
|
+
files: counters.files,
|
|
29
|
+
truncatedByDepth: counters.truncatedByDepth,
|
|
30
|
+
truncatedByLimit: counters.truncatedByLimit,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function walk(currentPath, prefix, currentDepth) {
|
|
34
|
+
if (counters.visibleNodes >= limit) {
|
|
35
|
+
counters.truncatedByLimit = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const visibleEntries = entries
|
|
47
|
+
.filter((entry) => !shouldSkipDirectory(entry.name))
|
|
48
|
+
.sort(sortEntries);
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < visibleEntries.length; index += 1) {
|
|
51
|
+
if (counters.visibleNodes >= limit) {
|
|
52
|
+
counters.truncatedByLimit = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entry = visibleEntries[index];
|
|
57
|
+
const isLast = index === visibleEntries.length - 1;
|
|
58
|
+
const branch = isLast ? '└── ' : '├── ';
|
|
59
|
+
const childPrefix = `${prefix}${isLast ? ' ' : '│ '}`;
|
|
60
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
61
|
+
const label = entry.isDirectory()
|
|
62
|
+
? formatDirectoryLabel(`${entry.name}/`)
|
|
63
|
+
: entry.name;
|
|
64
|
+
|
|
65
|
+
lines.push(`${prefix}${branch}${label}`);
|
|
66
|
+
counters.visibleNodes += 1;
|
|
67
|
+
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
counters.directories += 1;
|
|
70
|
+
} else {
|
|
71
|
+
counters.files += 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!entry.isDirectory()) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (currentDepth >= depth) {
|
|
79
|
+
if (await hasVisibleChildren(fullPath)) {
|
|
80
|
+
counters.truncatedByDepth = true;
|
|
81
|
+
lines.push(`${childPrefix}${formatHint('└── ... mas dentro')}`);
|
|
82
|
+
counters.visibleNodes += 1;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await walk(fullPath, childPrefix, currentDepth + 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatDirectoryLabel(value) {
|
|
93
|
+
if (!process.stdout.isTTY) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return `${COLORS.folder}${value}${COLORS.reset}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatHint(value) {
|
|
101
|
+
if (!process.stdout.isTTY) {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return `${COLORS.hint}${value}${COLORS.reset}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function hasVisibleChildren(directoryPath) {
|
|
109
|
+
try {
|
|
110
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
111
|
+
return entries.some((entry) => !shouldSkipDirectory(entry.name));
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sortEntries(left, right) {
|
|
118
|
+
if (left.isDirectory() && !right.isDirectory()) {
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!left.isDirectory() && right.isDirectory()) {
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return left.name.localeCompare(right.name, 'es', { sensitivity: 'base' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shouldSkipDirectory(name) {
|
|
130
|
+
return name === '.git' || name === 'node_modules';
|
|
131
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const COLORS = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
python: '\x1b[34m',
|
|
7
|
+
node: '\x1b[31m',
|
|
8
|
+
rust: '\x1b[33m',
|
|
9
|
+
go: '\x1b[36m',
|
|
10
|
+
java: '\x1b[35m',
|
|
11
|
+
php: '\x1b[95m',
|
|
12
|
+
ruby: '\x1b[91m',
|
|
13
|
+
generic: '\x1b[92m',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function detectProjectContext(cwd = process.cwd()) {
|
|
17
|
+
const entries = await safeReadDir(cwd);
|
|
18
|
+
if (!entries.length) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
23
|
+
const hasPyFiles = entries.some((entry) => !entry.isDirectory() && entry.name.endsWith('.py'));
|
|
24
|
+
const hasJsFiles = entries.some((entry) => !entry.isDirectory() && isNodeScript(entry.name));
|
|
25
|
+
const hasPhpFiles = entries.some((entry) => !entry.isDirectory() && entry.name.endsWith('.php'));
|
|
26
|
+
const hasRubyFiles = entries.some((entry) => !entry.isDirectory() && entry.name.endsWith('.rb'));
|
|
27
|
+
|
|
28
|
+
if (names.has('pyproject.toml') || names.has('requirements.txt') || names.has('Pipfile') || names.has('setup.py') || hasPyFiles) {
|
|
29
|
+
return createContext('python', cwd, 'PYTHON');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (names.has('package.json') || names.has('node_modules') || hasJsFiles) {
|
|
33
|
+
return createContext('node', cwd, 'NODE');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (names.has('Cargo.toml')) {
|
|
37
|
+
return createContext('rust', cwd, 'RUST');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (names.has('go.mod')) {
|
|
41
|
+
return createContext('go', cwd, 'GO');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (names.has('pom.xml') || names.has('build.gradle') || names.has('build.gradle.kts')) {
|
|
45
|
+
return createContext('java', cwd, 'JAVA');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (names.has('composer.json') || hasPhpFiles) {
|
|
49
|
+
return createContext('php', cwd, 'PHP');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (names.has('Gemfile') || hasRubyFiles) {
|
|
53
|
+
return createContext('ruby', cwd, 'RUBY');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatProjectContext(context) {
|
|
60
|
+
const folderName = path.basename(context.cwd) || context.cwd;
|
|
61
|
+
const badge = process.stdout.isTTY
|
|
62
|
+
? `${COLORS[context.type] || COLORS.generic}[${context.label}]${COLORS.reset}`
|
|
63
|
+
: `[${context.label}]`;
|
|
64
|
+
|
|
65
|
+
return `Contexto: ${folderName} ${badge}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createContext(type, cwd, label) {
|
|
69
|
+
return { type, cwd, label };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function safeReadDir(directoryPath) {
|
|
73
|
+
try {
|
|
74
|
+
return await fs.readdir(directoryPath, { withFileTypes: true });
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isNodeScript(name) {
|
|
81
|
+
return name.endsWith('.js') || name.endsWith('.mjs') || name.endsWith('.cjs') || name.endsWith('.ts');
|
|
82
|
+
}
|