dex-termux-cli 0.3.0-beta.1 → 0.3.0-beta.3
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 +3 -3
- package/package.json +9 -6
- package/src/app/main.js +4 -2
- package/src/commands/android-shell.js +192 -41
- package/src/commands/menu.js +5 -4
- package/src/commands/version.js +84 -11
- package/src/core/args.js +3 -3
- package/src/core/config.js +4 -3
- package/src/core/scopes.js +10 -0
- package/src/ui/output.js +8 -8
- package/src/ui/prompt.js +2 -2
- package/src/utils/platform.js +86 -1
- package/src/utils/project-context.js +103 -10
package/README.md
CHANGED
|
@@ -35,13 +35,13 @@ Dex mejora tareas comunes de terminal sin tapar el entorno real. Sirve para expl
|
|
|
35
35
|
- `dex -b`: busqueda guiada
|
|
36
36
|
- `dex -e`: explicacion de comandos comunes
|
|
37
37
|
- `dex -t`: tree guiado
|
|
38
|
-
- `dex -a`: shell redisenada para Android storage en
|
|
38
|
+
- `dex -a`: shell de acceso rapido redisenada para Android storage en Termux o para HOME en Linux
|
|
39
39
|
- `dex -i`: instalacion del proyecto actual incluso desde subcarpetas del proyecto
|
|
40
40
|
- `dex -r`: shell segura del proyecto actual incluso desde subcarpetas del proyecto
|
|
41
41
|
|
|
42
42
|
## Contexto del proyecto
|
|
43
43
|
|
|
44
|
-
Dex detecta el lenguaje segun los archivos del directorio actual. El badge compacto del prompt usa formato limpio como `PYTHON`, `NODE` o `PHP`. La version detectada del runtime se ve en `dex -c`, no dentro del badge.
|
|
44
|
+
Dex detecta el lenguaje segun los archivos del directorio actual y tambien desde estructuras simples como `src/main.py` o `app/main.js`. El badge compacto del prompt usa formato limpio como `PYTHON`, `NODE` o `PHP`. La version detectada del runtime se ve en `dex -c`, no dentro del badge.
|
|
45
45
|
|
|
46
46
|
Lenguajes detectables actualmente:
|
|
47
47
|
|
|
@@ -78,7 +78,7 @@ El modo actual se guarda en `~/.config/dex/config.json`.
|
|
|
78
78
|
## Estado real de la beta
|
|
79
79
|
|
|
80
80
|
- `dex -m`, `-a`, `-b`, `-t`, `-v`, `-c` y `--prompt-context` funcionan
|
|
81
|
-
- `dex -a` ahora abre una
|
|
81
|
+
- `dex -a` ahora abre una shell de acceso rapido con prompt propio y atajos. En Termux apunta a Android storage y en Linux apunta a HOME
|
|
82
82
|
- `dex -i` funciona en proyectos validos y ahora usa el root detectado del proyecto aunque entres desde una subcarpeta
|
|
83
83
|
- `dex -r` funciona para proyectos soportados, hereda mejor el root del proyecto y en Termux exige Android storage
|
|
84
84
|
- el modo seguro real ya cubre Python, Node, PHP, Ruby, Go, Rust y Java
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dex-termux-cli",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Visual CLI for Termux and Linux with guided search, readable tree views, project context, version checks, and safe flows for Python, Node, and
|
|
5
|
+
"description": "Visual CLI for Termux and Linux with guided search, readable tree views, project context, version checks, Android mode, and safe flows for Python, Node, PHP, Ruby, Go, Rust, and Java.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"termux",
|
|
8
|
+
"linux",
|
|
8
9
|
"android",
|
|
9
10
|
"cli",
|
|
10
11
|
"python",
|
|
11
12
|
"node",
|
|
12
13
|
"php",
|
|
14
|
+
"ruby",
|
|
15
|
+
"go",
|
|
16
|
+
"rust",
|
|
17
|
+
"java",
|
|
13
18
|
"safe-shell"
|
|
14
19
|
],
|
|
15
20
|
"license": "MIT",
|
|
@@ -18,7 +23,8 @@
|
|
|
18
23
|
},
|
|
19
24
|
"scripts": {
|
|
20
25
|
"start": "node ./bin/dex",
|
|
21
|
-
"help": "node ./bin/dex --help"
|
|
26
|
+
"help": "node ./bin/dex --help",
|
|
27
|
+
"test": "node --test ./tests/**/*.test.js"
|
|
22
28
|
},
|
|
23
29
|
"author": "farllirs",
|
|
24
30
|
"files": [
|
|
@@ -38,8 +44,5 @@
|
|
|
38
44
|
},
|
|
39
45
|
"engines": {
|
|
40
46
|
"node": ">=18"
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"tree": "^0.1.3"
|
|
44
47
|
}
|
|
45
48
|
}
|
package/src/app/main.js
CHANGED
|
@@ -11,10 +11,12 @@ import { runTreeCommand } from '../commands/tree.js';
|
|
|
11
11
|
import { runVersionCommand } from '../commands/version.js';
|
|
12
12
|
import { printHelp, printProjectContextLine } from '../ui/output.js';
|
|
13
13
|
import { detectProjectContext, formatProjectContext } from '../utils/project-context.js';
|
|
14
|
+
import { resolvePlatformMode } from '../utils/platform.js';
|
|
14
15
|
|
|
15
16
|
export async function main(argv = process.argv.slice(2)) {
|
|
16
17
|
const parsed = parseArgs(argv);
|
|
17
18
|
const config = await loadUserConfig();
|
|
19
|
+
const platformMode = resolvePlatformMode(config);
|
|
18
20
|
const isPromptOnly = parsed.command === 'prompt-context' || parsed.command === 'prompt-project-root' || parsed.command === 'prompt-project-path';
|
|
19
21
|
const isContextOnly = parsed.command === 'context';
|
|
20
22
|
const isVersionOnly = parsed.command === 'version';
|
|
@@ -27,7 +29,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
if (parsed.help || !argv.length) {
|
|
30
|
-
printHelp();
|
|
32
|
+
printHelp(platformMode);
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -91,5 +93,5 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
printHelp();
|
|
96
|
+
printHelp(platformMode);
|
|
95
97
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { spawn } from 'node:child_process';
|
|
3
5
|
import { getUserConfigPath, loadUserConfig } from '../core/config.js';
|
|
4
6
|
import { resolveInteractiveShell } from '../utils/shell.js';
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getQuickAccessAliases,
|
|
9
|
+
getQuickAccessLabel,
|
|
10
|
+
getQuickAccessModeDescription,
|
|
11
|
+
getQuickAccessRoot,
|
|
12
|
+
getQuickAccessShortcutSummary,
|
|
13
|
+
getQuickAccessTitle,
|
|
14
|
+
resolvePlatformMode,
|
|
15
|
+
} from '../utils/platform.js';
|
|
6
16
|
|
|
7
17
|
const ANDROID_BASH_CANDIDATES = [
|
|
8
18
|
'/data/data/com.termux/files/usr/bin/bash',
|
|
@@ -14,63 +24,64 @@ export async function runAndroidShellCommand() {
|
|
|
14
24
|
|
|
15
25
|
if (!config.features.androidShortcut) {
|
|
16
26
|
throw new Error(
|
|
17
|
-
`
|
|
27
|
+
`El acceso rapido esta desactivado. Activa features.androidShortcut en ${getUserConfigPath()}`,
|
|
18
28
|
);
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
const platformMode = resolvePlatformMode(config);
|
|
22
|
-
|
|
23
|
-
if (!canUseAndroidFeatures(platformMode)) {
|
|
24
|
-
throw new Error('dex -a solo esta disponible en modo Termux/Android. Cambia el modo Linux desde ajustes si quieres evitar esta opcion.');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const androidRoot = getAndroidStorageRoot(platformMode);
|
|
32
|
+
const quickAccessRoot = getQuickAccessRoot(platformMode);
|
|
28
33
|
|
|
29
34
|
try {
|
|
30
|
-
await fs.access(
|
|
35
|
+
await fs.access(quickAccessRoot);
|
|
31
36
|
} catch {
|
|
32
|
-
throw new Error(`No se puede acceder a la ruta
|
|
37
|
+
throw new Error(`No se puede acceder a la ruta base de acceso rapido: ${quickAccessRoot}`);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const interactiveShell = await resolveAndroidInteractiveShell(platformMode);
|
|
41
|
+
const shellSession = await createQuickAccessShellSession(platformMode, interactiveShell);
|
|
36
42
|
|
|
37
|
-
console.log(
|
|
43
|
+
console.log(getQuickAccessTitle(platformMode));
|
|
38
44
|
console.log('');
|
|
39
|
-
console.log(`Raiz : ${
|
|
45
|
+
console.log(`Raiz : ${quickAccessRoot}`);
|
|
40
46
|
console.log(`Shell : ${interactiveShell.shellName} interactiva`);
|
|
41
|
-
console.log(
|
|
42
|
-
console.log(
|
|
47
|
+
console.log(`Modo : ${getQuickAccessModeDescription(platformMode)}`);
|
|
48
|
+
console.log(`Atajos : ${getQuickAccessShortcutSummary(platformMode)}`);
|
|
43
49
|
console.log('Salida : escribe exit para volver');
|
|
44
50
|
console.log('');
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
try {
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
const child = spawn(shellSession.shellPath, shellSession.shellArgs, {
|
|
55
|
+
cwd: quickAccessRoot,
|
|
56
|
+
stdio: 'inherit',
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
...shellSession.env,
|
|
60
|
+
DEX_CONTEXT: platformMode === 'termux' ? 'android-shell' : 'linux-shell',
|
|
61
|
+
DEX_ANDROID_ROOT: quickAccessRoot,
|
|
62
|
+
DEX_ANDROID_LABEL: getQuickAccessLabel(platformMode),
|
|
63
|
+
DEX_ANDROID_SHELL: interactiveShell.shellName,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.on('error', reject);
|
|
68
|
+
child.on('exit', (code, signal) => {
|
|
69
|
+
if (signal) {
|
|
70
|
+
reject(new Error(`La shell de acceso rapido termino por senal: ${signal}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (code && code !== 0) {
|
|
75
|
+
reject(new Error(`La shell de acceso rapido termino con codigo ${code}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
72
81
|
});
|
|
73
|
-
}
|
|
82
|
+
} finally {
|
|
83
|
+
await shellSession.cleanup();
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
async function resolveAndroidInteractiveShell(platformMode) {
|
|
@@ -95,3 +106,143 @@ async function resolveAndroidInteractiveShell(platformMode) {
|
|
|
95
106
|
|
|
96
107
|
return interactiveShell;
|
|
97
108
|
}
|
|
109
|
+
|
|
110
|
+
async function createQuickAccessShellSession(platformMode, interactiveShell) {
|
|
111
|
+
const tmpBase = await fs.mkdtemp(path.join(os.tmpdir(), 'dex-shell-'));
|
|
112
|
+
const aliases = getQuickAccessAliases(platformMode);
|
|
113
|
+
const root = getQuickAccessRoot(platformMode);
|
|
114
|
+
const label = getQuickAccessLabel(platformMode);
|
|
115
|
+
const rcContent = buildShellRc({
|
|
116
|
+
aliases,
|
|
117
|
+
root,
|
|
118
|
+
label,
|
|
119
|
+
shellName: interactiveShell.shellName,
|
|
120
|
+
platformMode,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (interactiveShell.shellName === 'bash') {
|
|
124
|
+
const rcPath = path.join(tmpBase, 'dex-bashrc');
|
|
125
|
+
await fs.writeFile(rcPath, rcContent, 'utf8');
|
|
126
|
+
return {
|
|
127
|
+
shellPath: interactiveShell.shellPath,
|
|
128
|
+
shellArgs: ['--rcfile', rcPath, '-i'],
|
|
129
|
+
env: {},
|
|
130
|
+
cleanup: () => fs.rm(tmpBase, { recursive: true, force: true }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (interactiveShell.shellName === 'zsh') {
|
|
135
|
+
const zdotdir = path.join(tmpBase, 'zsh');
|
|
136
|
+
await fs.mkdir(zdotdir, { recursive: true });
|
|
137
|
+
await fs.writeFile(path.join(zdotdir, '.zshrc'), rcContent, 'utf8');
|
|
138
|
+
return {
|
|
139
|
+
shellPath: interactiveShell.shellPath,
|
|
140
|
+
shellArgs: ['-i'],
|
|
141
|
+
env: { ZDOTDIR: zdotdir },
|
|
142
|
+
cleanup: () => fs.rm(tmpBase, { recursive: true, force: true }),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rcPath = path.join(tmpBase, 'dex-shrc');
|
|
147
|
+
await fs.writeFile(rcPath, rcContent, 'utf8');
|
|
148
|
+
return {
|
|
149
|
+
shellPath: interactiveShell.shellPath,
|
|
150
|
+
shellArgs: ['-i'],
|
|
151
|
+
env: { ENV: rcPath },
|
|
152
|
+
cleanup: () => fs.rm(tmpBase, { recursive: true, force: true }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildShellRc({ aliases, root, label, shellName, platformMode }) {
|
|
157
|
+
if (platformMode === 'termux') {
|
|
158
|
+
return buildTermuxShellRc(shellName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return buildLinuxShellRc({ aliases, root, label, shellName });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildTermuxShellRc(shellName) {
|
|
165
|
+
const lines = [
|
|
166
|
+
'# dex quick access shell',
|
|
167
|
+
'export PATH="$HOME/bin:$PATH"',
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
if (shellName === 'zsh') {
|
|
171
|
+
lines.push('[[ -f "$HOME/.zshrc" ]] && source "$HOME/.zshrc"');
|
|
172
|
+
lines.push('printf "Dex: acceso rapido cargado en %s\\n" "$PWD"');
|
|
173
|
+
return lines.join('\n') + '\n';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (shellName === 'bash') {
|
|
177
|
+
lines.push('[[ -f "$HOME/.bashrc" ]] && source "$HOME/.bashrc"');
|
|
178
|
+
lines.push('printf "Dex: acceso rapido cargado en %s\\n" "$PWD"');
|
|
179
|
+
return lines.join('\n') + '\n';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
lines.push('printf "Dex: acceso rapido cargado en %s\\n" "$PWD"');
|
|
183
|
+
return lines.join('\n') + '\n';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildLinuxShellRc({ aliases, root, label, shellName }) {
|
|
187
|
+
const aliasLines = Object.entries(aliases).map(([name, target]) => `alias ${name}='cd "${target}"'`);
|
|
188
|
+
const commonLines = [
|
|
189
|
+
'# dex quick access shell',
|
|
190
|
+
'export PATH="$HOME/bin:$PATH"',
|
|
191
|
+
`export DEX_QUICK_ROOT="${root}"`,
|
|
192
|
+
`export DEX_QUICK_LABEL="${label}"`,
|
|
193
|
+
...aliasLines,
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
if (shellName === 'zsh') {
|
|
197
|
+
return [
|
|
198
|
+
...commonLines,
|
|
199
|
+
'[[ -f "$HOME/.zshrc" ]] && source "$HOME/.zshrc"',
|
|
200
|
+
'_dex_linux_project_badge_zsh() {',
|
|
201
|
+
' local dex_bin context',
|
|
202
|
+
' dex_bin="$(command -v dex 2>/dev/null)"',
|
|
203
|
+
' [[ -z "$dex_bin" ]] && return',
|
|
204
|
+
' context="$($dex_bin --prompt-context 2>/dev/null)"',
|
|
205
|
+
' [[ -z "$context" ]] && return',
|
|
206
|
+
' print -n "%F{81}[$context]%f"',
|
|
207
|
+
'}',
|
|
208
|
+
'_dex_linux_prompt_zsh() {',
|
|
209
|
+
' local badge',
|
|
210
|
+
' badge="$(_dex_linux_project_badge_zsh)"',
|
|
211
|
+
" PROMPT=$'%F{45}Dex@linux%f %F{117}%~%f\\n%F{45}>%f '",
|
|
212
|
+
' RPROMPT="$badge"',
|
|
213
|
+
'}',
|
|
214
|
+
'autoload -Uz add-zsh-hook 2>/dev/null || true',
|
|
215
|
+
'add-zsh-hook precmd _dex_linux_prompt_zsh 2>/dev/null || true',
|
|
216
|
+
'_dex_linux_prompt_zsh',
|
|
217
|
+
'printf "Dex: acceso rapido cargado en %s\\n" "$PWD"',
|
|
218
|
+
].join('\n') + '\n';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (shellName === 'bash') {
|
|
222
|
+
return [
|
|
223
|
+
...commonLines,
|
|
224
|
+
'_dex_linux_project_badge_bash() {',
|
|
225
|
+
' local dex_bin context',
|
|
226
|
+
' dex_bin="$(command -v dex 2>/dev/null)"',
|
|
227
|
+
' [[ -z "$dex_bin" ]] && return',
|
|
228
|
+
' context="$($dex_bin --prompt-context 2>/dev/null)"',
|
|
229
|
+
' [[ -z "$context" ]] && return',
|
|
230
|
+
' printf "\\[\\e[38;5;81m\\][%s]\\[\\e[0m\\]" "$context"',
|
|
231
|
+
'}',
|
|
232
|
+
'_dex_linux_prompt_bash() {',
|
|
233
|
+
' local badge',
|
|
234
|
+
' badge="$(_dex_linux_project_badge_bash)"',
|
|
235
|
+
' PS1="\\[\\e[38;5;45m\\]Dex@linux\\[\\e[0m\\] \\[\\e[38;5;117m\\]\\w\\[\\e[0m\\] ${badge}\\n\\[\\e[38;5;45m\\]>\\[\\e[0m\\] "',
|
|
236
|
+
'}',
|
|
237
|
+
'PROMPT_COMMAND=_dex_linux_prompt_bash',
|
|
238
|
+
'_dex_linux_prompt_bash',
|
|
239
|
+
'printf "Dex: acceso rapido cargado en %s\\n" "$PWD"',
|
|
240
|
+
].join('\n') + '\n';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return [
|
|
244
|
+
...commonLines,
|
|
245
|
+
`export PS1='[dex ${label}] \\w \\$ '`,
|
|
246
|
+
'printf "Dex: acceso rapido cargado en %s\\n" "$PWD"',
|
|
247
|
+
].join('\n') + '\n';
|
|
248
|
+
}
|
package/src/commands/menu.js
CHANGED
|
@@ -27,7 +27,8 @@ export async function runMenuCommand() {
|
|
|
27
27
|
const action = await chooseMenuAction();
|
|
28
28
|
|
|
29
29
|
if (action === 'help') {
|
|
30
|
-
|
|
30
|
+
const config = await loadUserConfig();
|
|
31
|
+
printHelp(resolvePlatformMode(config));
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -101,8 +102,8 @@ async function runSettingsMenu() {
|
|
|
101
102
|
if (action === 'toggle-android-shortcut') {
|
|
102
103
|
const toggle = await chooseFeatureToggle(
|
|
103
104
|
config.features.androidShortcut,
|
|
104
|
-
'Acceso rapido
|
|
105
|
-
'permite usar dex -a',
|
|
105
|
+
'Acceso rapido',
|
|
106
|
+
'permite usar dex -a en Termux o Linux',
|
|
106
107
|
);
|
|
107
108
|
|
|
108
109
|
if (toggle === 'back') {
|
|
@@ -111,7 +112,7 @@ async function runSettingsMenu() {
|
|
|
111
112
|
|
|
112
113
|
const enabled = toggle === 'enable';
|
|
113
114
|
await setFeatureEnabled('androidShortcut', enabled);
|
|
114
|
-
console.log(`Acceso rapido
|
|
115
|
+
console.log(`Acceso rapido: ${enabled ? 'activado' : 'desactivado'}.`);
|
|
115
116
|
console.log('');
|
|
116
117
|
continue;
|
|
117
118
|
}
|
package/src/commands/version.js
CHANGED
|
@@ -69,14 +69,14 @@ async function readLatestPublishedVersion() {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function compareVersions(left, right) {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const maxLength = Math.max(
|
|
72
|
+
export function compareVersions(left, right) {
|
|
73
|
+
const leftVersion = parseSemver(left);
|
|
74
|
+
const rightVersion = parseSemver(right);
|
|
75
|
+
const maxLength = Math.max(leftVersion.core.length, rightVersion.core.length);
|
|
76
76
|
|
|
77
77
|
for (let index = 0; index < maxLength; index += 1) {
|
|
78
|
-
const leftValue =
|
|
79
|
-
const rightValue =
|
|
78
|
+
const leftValue = leftVersion.core[index] || 0;
|
|
79
|
+
const rightValue = rightVersion.core[index] || 0;
|
|
80
80
|
|
|
81
81
|
if (leftValue > rightValue) {
|
|
82
82
|
return 1;
|
|
@@ -87,12 +87,85 @@ function compareVersions(left, right) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
return comparePrerelease(leftVersion.prerelease, rightVersion.prerelease);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseSemver(version) {
|
|
94
|
+
const normalized = String(version).trim();
|
|
95
|
+
const [mainPart = '', prereleasePart = ''] = normalized.split('-', 2);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
core: mainPart
|
|
99
|
+
.split('.')
|
|
100
|
+
.map((part) => Number.parseInt(part, 10))
|
|
101
|
+
.filter((part) => Number.isInteger(part)),
|
|
102
|
+
prerelease: prereleasePart
|
|
103
|
+
? prereleasePart
|
|
104
|
+
.split('.')
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((part) => (/^\d+$/.test(part) ? Number.parseInt(part, 10) : part.toLowerCase()))
|
|
107
|
+
: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function comparePrerelease(left, right) {
|
|
112
|
+
const leftHasPrerelease = left.length > 0;
|
|
113
|
+
const rightHasPrerelease = right.length > 0;
|
|
114
|
+
|
|
115
|
+
if (!leftHasPrerelease && !rightHasPrerelease) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!leftHasPrerelease) {
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!rightHasPrerelease) {
|
|
124
|
+
return -1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const maxLength = Math.max(left.length, right.length);
|
|
128
|
+
|
|
129
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
130
|
+
const leftPart = left[index];
|
|
131
|
+
const rightPart = right[index];
|
|
132
|
+
|
|
133
|
+
if (leftPart === undefined) {
|
|
134
|
+
return -1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (rightPart === undefined) {
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const comparison = comparePrereleasePart(leftPart, rightPart);
|
|
142
|
+
if (comparison !== 0) {
|
|
143
|
+
return comparison;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
90
147
|
return 0;
|
|
91
148
|
}
|
|
92
149
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
150
|
+
function comparePrereleasePart(left, right) {
|
|
151
|
+
const leftIsNumber = typeof left === 'number';
|
|
152
|
+
const rightIsNumber = typeof right === 'number';
|
|
153
|
+
|
|
154
|
+
if (leftIsNumber && rightIsNumber) {
|
|
155
|
+
return left === right ? 0 : left > right ? 1 : -1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (leftIsNumber) {
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (rightIsNumber) {
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (left === right) {
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return left > right ? 1 : -1;
|
|
98
171
|
}
|
package/src/core/args.js
CHANGED
|
@@ -47,7 +47,7 @@ export function parseArgs(argv) {
|
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
if (token === '-b' || token === '--buscar') {
|
|
50
|
+
if (token === '-b' || token === '--buscar' || token === '--search' || token === 'search') {
|
|
51
51
|
parsed.command = 'search';
|
|
52
52
|
const next = argv[index + 1];
|
|
53
53
|
if (next && !next.startsWith('-')) {
|
|
@@ -57,7 +57,7 @@ export function parseArgs(argv) {
|
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
if (token === '-e' || token === '--explicar' || token === 'explicar') {
|
|
60
|
+
if (token === '-e' || token === '--explicar' || token === '--explain' || token === 'explicar' || token === 'explain') {
|
|
61
61
|
parsed.command = 'explain';
|
|
62
62
|
const next = argv[index + 1];
|
|
63
63
|
if (next && !next.startsWith('-')) {
|
|
@@ -87,7 +87,7 @@ export function parseArgs(argv) {
|
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
if (token === '-r' || token === '--seguro' || token === '--safe-shell' || token === 'seguro') {
|
|
90
|
+
if (token === '-r' || token === '--seguro' || token === '--safe-shell' || token === 'seguro' || token === 'safe-shell') {
|
|
91
91
|
parsed.command = 'safe-shell';
|
|
92
92
|
continue;
|
|
93
93
|
}
|
package/src/core/config.js
CHANGED
|
@@ -4,8 +4,8 @@ import { getHomeDirectory, normalizePlatformMode } from '../utils/platform.js';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_CONFIG = {
|
|
6
6
|
features: {
|
|
7
|
-
androidShortcut:
|
|
8
|
-
projectBadge:
|
|
7
|
+
androidShortcut: true,
|
|
8
|
+
projectBadge: true,
|
|
9
9
|
smartProjectInstall: false,
|
|
10
10
|
},
|
|
11
11
|
ui: {
|
|
@@ -82,13 +82,14 @@ export async function setPlatformMode(platformMode) {
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
if (normalized === 'linux') {
|
|
85
|
-
nextFeatures.androidShortcut =
|
|
85
|
+
nextFeatures.androidShortcut = true;
|
|
86
86
|
nextFeatures.projectBadge = true;
|
|
87
87
|
nextFeatures.smartProjectInstall = true;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
if (normalized === 'termux') {
|
|
91
91
|
nextFeatures.androidShortcut = true;
|
|
92
|
+
nextFeatures.projectBadge = true;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
return saveUserConfig({
|
package/src/core/scopes.js
CHANGED
|
@@ -30,6 +30,16 @@ export function getScopeOptions(platformMode) {
|
|
|
30
30
|
root: getAndroidStorageRoot(platformMode),
|
|
31
31
|
description: 'Busca dentro del almacenamiento compartido accesible.',
|
|
32
32
|
});
|
|
33
|
+
} else {
|
|
34
|
+
const sharedRoot = getAndroidStorageRoot(platformMode);
|
|
35
|
+
if (sharedRoot) {
|
|
36
|
+
options.push({
|
|
37
|
+
key: 'shared',
|
|
38
|
+
label: 'Almacenamiento compartido',
|
|
39
|
+
root: sharedRoot,
|
|
40
|
+
description: 'Busca dentro del almacenamiento compartido montado en Linux.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
return options;
|
package/src/ui/output.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getUserConfigPath } from '../core/config.js';
|
|
3
|
+
import { getScopeOptions } from '../core/scopes.js';
|
|
4
|
+
import { isHostTermux } from '../utils/platform.js';
|
|
3
5
|
|
|
4
6
|
const ANSI = {
|
|
5
7
|
bold: '\x1b[1m',
|
|
@@ -9,7 +11,7 @@ const ANSI = {
|
|
|
9
11
|
reset: '\x1b[0m',
|
|
10
12
|
};
|
|
11
13
|
|
|
12
|
-
export function printHelp() {
|
|
14
|
+
export function printHelp(platformMode = '') {
|
|
13
15
|
printBanner();
|
|
14
16
|
console.log('');
|
|
15
17
|
console.log(' Explorar, entender y preparar proyectos desde Termux o Linux.');
|
|
@@ -25,7 +27,7 @@ export function printHelp() {
|
|
|
25
27
|
'dex -t src arbol guiado de una ruta',
|
|
26
28
|
'dex -i instalar dependencias del proyecto detectado',
|
|
27
29
|
'dex -r abrir modo seguro del proyecto (Py/Node/PHP/Ruby/Go/Rust/Java)',
|
|
28
|
-
'dex -a acceso rapido a Android',
|
|
30
|
+
'dex -a acceso rapido a Android o Linux',
|
|
29
31
|
]);
|
|
30
32
|
|
|
31
33
|
printBlock('Uso rapido', [
|
|
@@ -33,14 +35,12 @@ export function printHelp() {
|
|
|
33
35
|
'dex -t . --depth 3 --scope actual',
|
|
34
36
|
'dex -c',
|
|
35
37
|
'dex --prompt-context',
|
|
36
|
-
'dex-project-
|
|
38
|
+
'dex --prompt-project-root',
|
|
39
|
+
'dex --prompt-project-path',
|
|
37
40
|
]);
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'home tu HOME del entorno actual',
|
|
42
|
-
'android almacenamiento accesible en modo Termux',
|
|
43
|
-
]);
|
|
42
|
+
const scopeOptions = getScopeOptions(platformMode || (isHostTermux() ? 'termux' : 'linux'));
|
|
43
|
+
printBlock('Scopes', scopeOptions.map((option) => `${option.key.padEnd(19, ' ')}${option.description}`));
|
|
44
44
|
|
|
45
45
|
printBlock('Atajos', [
|
|
46
46
|
'-h --help ayuda',
|
package/src/ui/prompt.js
CHANGED
|
@@ -75,9 +75,9 @@ export async function chooseSettingsAction(config) {
|
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
key: 'toggle-android-shortcut',
|
|
78
|
-
label: 'Acceso rapido
|
|
78
|
+
label: 'Acceso rapido',
|
|
79
79
|
status: formatStatus(config.features.androidShortcut),
|
|
80
|
-
usage: 'permite dex -a en Termux',
|
|
80
|
+
usage: 'permite dex -a en Termux o Linux',
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
key: 'toggle-project-badge',
|
package/src/utils/platform.js
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
const TERMUX_HOME = '/data/data/com.termux/files/home';
|
|
4
5
|
const ANDROID_STORAGE = '/sdcard';
|
|
6
|
+
const SHARED_STORAGE_CANDIDATES = [
|
|
7
|
+
'/storage/emulated/0',
|
|
8
|
+
'/sdcard',
|
|
9
|
+
];
|
|
5
10
|
|
|
6
11
|
export function getHomeDirectory() {
|
|
7
12
|
return process.env.HOME || TERMUX_HOME;
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
export function getSharedStorageRoot() {
|
|
16
|
+
for (const candidate of SHARED_STORAGE_CANDIDATES) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(candidate)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export function isHostTermux() {
|
|
11
30
|
const home = getHomeDirectory();
|
|
12
31
|
return Boolean(process.env.TERMUX_VERSION) || home === TERMUX_HOME || home.startsWith(TERMUX_HOME + path.sep);
|
|
@@ -53,7 +72,73 @@ export function canUseAndroidFeatures(platformMode) {
|
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export function getAndroidStorageRoot(platformMode) {
|
|
56
|
-
|
|
75
|
+
if (canUseAndroidFeatures(platformMode)) {
|
|
76
|
+
return ANDROID_STORAGE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return getSharedStorageRoot();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getQuickAccessRoot(platformMode) {
|
|
83
|
+
const sharedStorage = getSharedStorageRoot();
|
|
84
|
+
|
|
85
|
+
if (platformMode === 'termux') {
|
|
86
|
+
return sharedStorage || ANDROID_STORAGE;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sharedStorage || getHomeDirectory();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getQuickAccessTitle(platformMode) {
|
|
93
|
+
return platformMode === 'termux' ? 'Dex Android' : 'Dex Linux';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getQuickAccessModeDescription(platformMode) {
|
|
97
|
+
return platformMode === 'termux'
|
|
98
|
+
? 'interfaz Android redisenada'
|
|
99
|
+
: 'interfaz Linux redisenada';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getQuickAccessLabel(platformMode) {
|
|
103
|
+
if (platformMode === 'termux') {
|
|
104
|
+
return 'ANDROID STORAGE';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return getSharedStorageRoot() ? 'SHARED STORAGE' : 'LINUX HOME';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getQuickAccessShortcutSummary(platformMode) {
|
|
111
|
+
return platformMode === 'termux'
|
|
112
|
+
? 'dl, docs, dcim, pics, music, movies, shared'
|
|
113
|
+
: 'shared, home, dl, docs, desk, pics, music, vids, tmp';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getQuickAccessAliases(platformMode) {
|
|
117
|
+
if (platformMode === 'termux') {
|
|
118
|
+
return {
|
|
119
|
+
shared: '/sdcard',
|
|
120
|
+
dl: '/sdcard/Download',
|
|
121
|
+
docs: '/sdcard/Documents',
|
|
122
|
+
dcim: '/sdcard/DCIM',
|
|
123
|
+
pics: '/sdcard/Pictures',
|
|
124
|
+
music: '/sdcard/Music',
|
|
125
|
+
movies: '/sdcard/Movies',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const home = getHomeDirectory();
|
|
130
|
+
const shared = getSharedStorageRoot() || home;
|
|
131
|
+
return {
|
|
132
|
+
home,
|
|
133
|
+
shared,
|
|
134
|
+
dl: path.join(shared, 'Download'),
|
|
135
|
+
docs: path.join(shared, 'Documents'),
|
|
136
|
+
desk: path.join(home, 'Desktop'),
|
|
137
|
+
pics: path.join(shared, 'Pictures'),
|
|
138
|
+
music: path.join(shared, 'Music'),
|
|
139
|
+
vids: path.join(shared, 'Movies'),
|
|
140
|
+
tmp: '/tmp',
|
|
141
|
+
};
|
|
57
142
|
}
|
|
58
143
|
|
|
59
144
|
export function shouldRestrictProjectToAndroidStorage(platformMode) {
|
|
@@ -24,6 +24,8 @@ const LANGUAGE_META = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const MAX_PARENT_LOOKUP = 16;
|
|
27
|
+
const MAX_IMPLICIT_SCAN_DEPTH = 3;
|
|
28
|
+
const MAX_IMPLICIT_SCAN_ENTRIES = 160;
|
|
27
29
|
|
|
28
30
|
export async function detectProjectContext(cwd = process.cwd()) {
|
|
29
31
|
const searchChain = buildSearchChain(cwd);
|
|
@@ -63,7 +65,7 @@ export function formatPromptContext(context) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export function formatPromptProjectRoot(context) {
|
|
66
|
-
return
|
|
68
|
+
return context.projectRoot || context.cwd;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export function formatPromptProjectPath(context) {
|
|
@@ -136,23 +138,68 @@ async function detectProjectContextAt(targetDir, cwd) {
|
|
|
136
138
|
return createContext('ruby', cwd, targetDir, await detectRubyVersion(targetDir), 'implicit');
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
const nestedImplicitType = await detectImplicitLanguageFromTree(targetDir);
|
|
142
|
+
if (nestedImplicitType) {
|
|
143
|
+
return createContext(nestedImplicitType, cwd, targetDir, await detectVersionForType(nestedImplicitType, targetDir), 'implicit');
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
return null;
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (let index = 1; index < detections.length; index += 1) {
|
|
146
|
-
const current = detections[index];
|
|
149
|
+
async function detectImplicitLanguageFromTree(rootDir) {
|
|
150
|
+
const queue = [{ dir: rootDir, depth: 0 }];
|
|
151
|
+
let visitedEntries = 0;
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
while (queue.length && visitedEntries < MAX_IMPLICIT_SCAN_ENTRIES) {
|
|
154
|
+
const current = queue.shift();
|
|
155
|
+
const nestedEntries = await safeReadDir(current.dir);
|
|
156
|
+
if (!nestedEntries.length) {
|
|
157
|
+
continue;
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
for (const item of nestedEntries) {
|
|
161
|
+
visitedEntries += 1;
|
|
162
|
+
|
|
163
|
+
if (!item.isDirectory()) {
|
|
164
|
+
if (item.name.endsWith('.py')) {
|
|
165
|
+
return 'python';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isNodeScript(item.name)) {
|
|
169
|
+
return 'node';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (item.name.endsWith('.php')) {
|
|
173
|
+
return 'php';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (item.name.endsWith('.rb')) {
|
|
177
|
+
return 'ruby';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (current.depth + 1 > MAX_IMPLICIT_SCAN_DEPTH) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (shouldSkipImplicitDirectory(item.name)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
queue.push({
|
|
192
|
+
dir: path.join(current.dir, item.name),
|
|
193
|
+
depth: current.depth + 1,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
153
196
|
}
|
|
154
197
|
|
|
155
|
-
return
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pickBestDetection(detections) {
|
|
202
|
+
return detections[0];
|
|
156
203
|
}
|
|
157
204
|
|
|
158
205
|
function createContext(type, cwd, projectRoot, version, detectionKind) {
|
|
@@ -279,6 +326,38 @@ async function detectJavaVersion(cwd) {
|
|
|
279
326
|
);
|
|
280
327
|
}
|
|
281
328
|
|
|
329
|
+
async function detectVersionForType(type, cwd) {
|
|
330
|
+
if (type === 'python') {
|
|
331
|
+
return detectPythonVersion(cwd);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (type === 'node') {
|
|
335
|
+
return detectNodeVersion(cwd);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (type === 'php') {
|
|
339
|
+
return detectPhpVersion(cwd);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (type === 'ruby') {
|
|
343
|
+
return detectRubyVersion(cwd);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (type === 'go') {
|
|
347
|
+
return detectGoVersion(cwd);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (type === 'rust') {
|
|
351
|
+
return detectRustVersion(cwd);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (type === 'java') {
|
|
355
|
+
return detectJavaVersion(cwd);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
282
361
|
async function safeReadDir(directoryPath) {
|
|
283
362
|
try {
|
|
284
363
|
return await fs.readdir(directoryPath, { withFileTypes: true });
|
|
@@ -334,3 +413,17 @@ function firstNonEmptyLine(content) {
|
|
|
334
413
|
function isNodeScript(name) {
|
|
335
414
|
return name.endsWith('.js') || name.endsWith('.mjs') || name.endsWith('.cjs') || name.endsWith('.ts');
|
|
336
415
|
}
|
|
416
|
+
|
|
417
|
+
function shouldSkipImplicitDirectory(name) {
|
|
418
|
+
return [
|
|
419
|
+
'.git',
|
|
420
|
+
'.venv',
|
|
421
|
+
'node_modules',
|
|
422
|
+
'__pycache__',
|
|
423
|
+
'.mypy_cache',
|
|
424
|
+
'.pytest_cache',
|
|
425
|
+
'.ruff_cache',
|
|
426
|
+
'dist',
|
|
427
|
+
'build',
|
|
428
|
+
].includes(name);
|
|
429
|
+
}
|