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 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 modo Termux
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 interfaz Android dedicada con splash, prompt propio y atajos (`shared`, `dl`, `docs`, `dcim`, `pics`, `music`, `movies`)
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.1",
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 PHP.",
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 { canUseAndroidFeatures, getAndroidStorageRoot, resolvePlatformMode } from '../utils/platform.js';
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
- `La opcion Android esta desactivada. Activa features.androidShortcut en ${getUserConfigPath()}`,
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(androidRoot);
35
+ await fs.access(quickAccessRoot);
31
36
  } catch {
32
- throw new Error(`No se puede acceder a la ruta Android: ${androidRoot}`);
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('Dex Android');
43
+ console.log(getQuickAccessTitle(platformMode));
38
44
  console.log('');
39
- console.log(`Raiz : ${androidRoot}`);
45
+ console.log(`Raiz : ${quickAccessRoot}`);
40
46
  console.log(`Shell : ${interactiveShell.shellName} interactiva`);
41
- console.log('Modo : interfaz Android redisenada');
42
- console.log('Atajos : dl, docs, dcim, pics, music, movies, shared');
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
- await new Promise((resolve, reject) => {
47
- const child = spawn(interactiveShell.shellPath, interactiveShell.shellArgs, {
48
- cwd: androidRoot,
49
- stdio: 'inherit',
50
- env: {
51
- ...process.env,
52
- DEX_CONTEXT: 'android-shell',
53
- DEX_ANDROID_ROOT: androidRoot,
54
- DEX_ANDROID_LABEL: 'ANDROID STORAGE',
55
- DEX_ANDROID_SHELL: interactiveShell.shellName,
56
- },
57
- });
58
-
59
- child.on('error', reject);
60
- child.on('exit', (code, signal) => {
61
- if (signal) {
62
- reject(new Error(`La shell Android termino por senal: ${signal}`));
63
- return;
64
- }
65
-
66
- if (code && code !== 0) {
67
- reject(new Error(`La shell Android termino con codigo ${code}`));
68
- return;
69
- }
70
-
71
- resolve();
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
+ }
@@ -27,7 +27,8 @@ export async function runMenuCommand() {
27
27
  const action = await chooseMenuAction();
28
28
 
29
29
  if (action === 'help') {
30
- printHelp();
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 a Android',
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 a Android: ${enabled ? 'activado' : 'desactivado'}.`);
115
+ console.log(`Acceso rapido: ${enabled ? 'activado' : 'desactivado'}.`);
115
116
  console.log('');
116
117
  continue;
117
118
  }
@@ -69,14 +69,14 @@ async function readLatestPublishedVersion() {
69
69
  }
70
70
  }
71
71
 
72
- function compareVersions(left, right) {
73
- const leftParts = normalizeVersion(left);
74
- const rightParts = normalizeVersion(right);
75
- const maxLength = Math.max(leftParts.length, rightParts.length);
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 = leftParts[index] || 0;
79
- const rightValue = rightParts[index] || 0;
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 normalizeVersion(version) {
94
- return String(version)
95
- .split('.')
96
- .map((part) => Number.parseInt(part.replace(/[^0-9].*$/, ''), 10))
97
- .filter((part) => Number.isInteger(part));
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
  }
@@ -4,8 +4,8 @@ import { getHomeDirectory, normalizePlatformMode } from '../utils/platform.js';
4
4
 
5
5
  const DEFAULT_CONFIG = {
6
6
  features: {
7
- androidShortcut: false,
8
- projectBadge: false,
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 = false;
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({
@@ -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-context --root',
38
+ 'dex --prompt-project-root',
39
+ 'dex --prompt-project-path',
37
40
  ]);
38
41
 
39
- printBlock('Scopes', [
40
- 'actual carpeta donde estas parado',
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 a Android',
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',
@@ -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
- return canUseAndroidFeatures(platformMode) ? ANDROID_STORAGE : '';
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 path.basename(context.projectRoot || context.cwd) || context.cwd;
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 pickBestDetection(detections) {
143
- let bestMatch = detections[0];
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
- if (current.type !== bestMatch.type) {
149
- break;
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
- bestMatch = current;
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 bestMatch;
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
+ }