dex-termux-cli 0.1.1 → 0.2.1

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
@@ -7,7 +7,7 @@
7
7
  <h1 align="center">Dex Termux CLI</h1>
8
8
 
9
9
  <p align="center">
10
- CLI visual para Termux con búsqueda guiada, árbol legible, explicación de comandos y flujos de instalación segura para proyectos dentro de Android storage.
10
+ CLI visual para Termux con búsqueda guiada, árbol legible, explicación de comandos, contexto de proyecto y flujos de instalación segura para proyectos dentro de Android storage.
11
11
  </p>
12
12
 
13
13
  <p align="center">
@@ -21,7 +21,7 @@
21
21
  <strong>Explorar.</strong> <strong>Entender.</strong> <strong>Instalar.</strong> <strong>Entrar al modo seguro.</strong>
22
22
  </p>
23
23
 
24
- > Estado actual: soporte seguro para Python, Node y PHP.
24
+ > Estado actual: soporte seguro para Python, Node y PHP, con lectura compacta del contexto del proyecto actual.
25
25
 
26
26
  ## Visión general
27
27
 
@@ -30,7 +30,7 @@ Dex está pensado para mejorar el trabajo diario en Termux cuando el proyecto vi
30
30
  ## En qué destaca
31
31
 
32
32
  - interfaz más legible para tareas comunes de terminal
33
- - detección del lenguaje del proyecto actual
33
+ - detección del lenguaje del proyecto actual con versión recortada
34
34
  - instalación segura para proyectos en Android storage
35
35
  - shell segura por proyecto con el contexto correcto
36
36
  - enfoque práctico en lugar de automatización opaca
@@ -40,6 +40,8 @@ Dex está pensado para mejorar el trabajo diario en Termux cuando el proyecto vi
40
40
  | Comando | Uso principal |
41
41
  | --- | --- |
42
42
  | `dex -m` | abre el menú principal |
43
+ | `dex -v` | muestra la versión local y revisa la publicada |
44
+ | `dex -c` | muestra el contexto del proyecto actual |
43
45
  | `dex -b` | busca archivos o carpetas |
44
46
  | `dex -e` | explica comandos comunes |
45
47
  | `dex -t` | muestra árbol guiado |
@@ -51,10 +53,14 @@ Dex está pensado para mejorar el trabajo diario en Termux cuando el proyecto vi
51
53
 
52
54
  - salida visual más clara que varios comandos crudos de terminal
53
55
  - exploración rápida de carpetas y archivos sin recordar tanta sintaxis
54
- - detección del lenguaje del proyecto actual
56
+ - detección del lenguaje del proyecto actual con una vista compacta útil para prompt
55
57
  - instalación segura cuando Android storage rompe el flujo normal
56
58
  - shell segura por proyecto para seguir trabajando con el entorno correcto
57
59
 
60
+ ## Contexto del proyecto
61
+
62
+ Dex puede detectar el tipo de proyecto desde la carpeta actual y resumirlo en una forma compacta como `PY 3.11`, `NODE 20` o `PHP 8.2`. Ese formato está pensado tanto para el comando `dex -c` como para integrarlo al prompt de la terminal.
63
+
58
64
  ## Instalación segura por lenguaje
59
65
 
60
66
  | Lenguaje | Estrategia de instalación | Modo seguro |
@@ -78,7 +84,7 @@ Dex puede instalar dependencias con `composer` en una copia segura del proyecto
78
84
 
79
85
  - instalación segura activa para Python, Node y PHP
80
86
  - shell segura activa para Python, Node y PHP
81
- - branding base ya integrado al repositorio
87
+ - menú y ayuda con una salida más clara
82
88
  - lectura del proyecto centrada en Termux y Android storage
83
89
 
84
90
  ## Roadmap
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dex-termux-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Termux CLI for Android with guided search, readable tree views, command help, and safe project install flows for Python, Node, and PHP.",
6
6
  "keywords": [
package/src/app/main.js CHANGED
@@ -1,20 +1,25 @@
1
1
  import { parseArgs } from '../core/args.js';
2
2
  import { loadUserConfig } from '../core/config.js';
3
3
  import { runAndroidShellCommand } from '../commands/android-shell.js';
4
+ import { runContextCommand } from '../commands/context.js';
4
5
  import { runExplainCommand } from '../commands/explain.js';
5
6
  import { runInstallCommand } from '../commands/install.js';
6
7
  import { runMenuCommand } from '../commands/menu.js';
7
8
  import { runSafeShellCommand } from '../commands/safe-shell.js';
8
9
  import { runSearchCommand } from '../commands/search.js';
9
10
  import { runTreeCommand } from '../commands/tree.js';
11
+ import { runVersionCommand } from '../commands/version.js';
10
12
  import { printHelp, printProjectContextLine } from '../ui/output.js';
11
13
  import { detectProjectContext, formatProjectContext } from '../utils/project-context.js';
12
14
 
13
15
  export async function main(argv = process.argv.slice(2)) {
14
16
  const parsed = parseArgs(argv);
15
17
  const config = await loadUserConfig();
18
+ const isPromptOnly = parsed.command === 'prompt-context';
19
+ const isContextOnly = parsed.command === 'context';
20
+ const isVersionOnly = parsed.command === 'version';
16
21
 
17
- if (config.features.projectBadge) {
22
+ if (config.features.projectBadge && !isPromptOnly && !isContextOnly && !isVersionOnly) {
18
23
  const context = await detectProjectContext();
19
24
  if (context) {
20
25
  printProjectContextLine(formatProjectContext(context));
@@ -26,6 +31,21 @@ export async function main(argv = process.argv.slice(2)) {
26
31
  return;
27
32
  }
28
33
 
34
+ if (parsed.command === 'version') {
35
+ await runVersionCommand();
36
+ return;
37
+ }
38
+
39
+ if (parsed.command === 'context') {
40
+ await runContextCommand();
41
+ return;
42
+ }
43
+
44
+ if (parsed.command === 'prompt-context') {
45
+ await runContextCommand({ prompt: true });
46
+ return;
47
+ }
48
+
29
49
  if (parsed.command === 'search') {
30
50
  await runSearchCommand(parsed);
31
51
  return;
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { getUserConfigPath, loadUserConfig } from '../core/config.js';
4
+ import { resolveInteractiveShell } from '../utils/shell.js';
4
5
 
5
6
  const ANDROID_ROOT = '/sdcard';
6
7
 
@@ -19,14 +20,15 @@ export async function runAndroidShellCommand() {
19
20
  throw new Error(`No se puede acceder a la ruta Android: ${ANDROID_ROOT}`);
20
21
  }
21
22
 
22
- const shell = process.env.SHELL || '/data/data/com.termux/files/usr/bin/sh';
23
+ const interactiveShell = await resolveInteractiveShell();
23
24
 
24
25
  console.log(`Abriendo shell en ${ANDROID_ROOT}`);
26
+ console.log(`Shell : ${interactiveShell.shellName} interactiva`);
25
27
  console.log('Escribe exit para volver.');
26
28
  console.log('');
27
29
 
28
30
  await new Promise((resolve, reject) => {
29
- const child = spawn(shell, {
31
+ const child = spawn(interactiveShell.shellPath, interactiveShell.shellArgs, {
30
32
  cwd: ANDROID_ROOT,
31
33
  stdio: 'inherit',
32
34
  env: {
@@ -0,0 +1,26 @@
1
+ import {
2
+ detectProjectContext,
3
+ formatProjectContext,
4
+ formatPromptContext,
5
+ formatProjectContextDetails,
6
+ } from '../utils/project-context.js';
7
+
8
+ export async function runContextCommand(options = {}) {
9
+ const context = await detectProjectContext();
10
+
11
+ if (!context) {
12
+ return;
13
+ }
14
+
15
+ if (options.prompt) {
16
+ const line = formatPromptContext(context);
17
+ if (line) {
18
+ process.stdout.write(line);
19
+ }
20
+ return;
21
+ }
22
+
23
+ console.log(formatProjectContext(context));
24
+ console.log('');
25
+ console.log(formatProjectContextDetails(context));
26
+ }
@@ -1,11 +1,14 @@
1
- import { loadUserConfig, setFeatureEnabled } from '../core/config.js';
1
+ import { loadUserConfig, setFeatureEnabled, setPromptContextPosition } from '../core/config.js';
2
+ import { runContextCommand } from './context.js';
2
3
  import { runExplainCommand } from './explain.js';
3
4
  import { runSafeShellCommand } from './safe-shell.js';
4
5
  import { runSearchCommand } from './search.js';
5
6
  import { runTreeCommand } from './tree.js';
7
+ import { runVersionCommand } from './version.js';
6
8
  import {
7
9
  chooseFeatureToggle,
8
10
  chooseMenuAction,
11
+ choosePromptContextPosition,
9
12
  chooseSearchPatternFromMenu,
10
13
  chooseSettingsAction,
11
14
  chooseTreeTargetFromMenu,
@@ -21,6 +24,16 @@ export async function runMenuCommand() {
21
24
  return;
22
25
  }
23
26
 
27
+ if (action === 'version') {
28
+ await runVersionCommand();
29
+ return;
30
+ }
31
+
32
+ if (action === 'context') {
33
+ await runContextCommand();
34
+ return;
35
+ }
36
+
24
37
  if (action === 'search') {
25
38
  const pattern = await chooseSearchPatternFromMenu();
26
39
  await runSearchCommand({ pattern, scope: '' });
@@ -100,6 +113,19 @@ async function runSettingsMenu() {
100
113
  continue;
101
114
  }
102
115
 
116
+ if (action === 'prompt-context-position') {
117
+ const position = await choosePromptContextPosition(config.ui?.promptContextPosition || 'right');
118
+
119
+ if (position === 'back') {
120
+ continue;
121
+ }
122
+
123
+ await setPromptContextPosition(position);
124
+ console.log('Posicion del contexto del prompt: ' + formatPromptContextPosition(position) + '.');
125
+ console.log('');
126
+ continue;
127
+ }
128
+
103
129
  if (action === 'toggle-smart-project-install') {
104
130
  const toggle = await chooseFeatureToggle(
105
131
  config.features.smartProjectInstall,
@@ -118,3 +144,15 @@ async function runSettingsMenu() {
118
144
  }
119
145
  }
120
146
  }
147
+
148
+ function formatPromptContextPosition(position) {
149
+ if (position === 'inline') {
150
+ return 'inline junto a la ruta';
151
+ }
152
+
153
+ if (position === 'off') {
154
+ return 'oculto';
155
+ }
156
+
157
+ return 'a la derecha';
158
+ }
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { loadUserConfig } from '../core/config.js';
6
6
  import { detectProjectContext } from '../utils/project-context.js';
7
+ import { resolveInteractiveShell } from '../utils/shell.js';
7
8
 
8
9
  export async function runSafeShellCommand() {
9
10
  const config = await loadUserConfig();
@@ -25,7 +26,7 @@ export async function runSafeShellCommand() {
25
26
 
26
27
  const projectKey = getProjectKey(projectRoot);
27
28
  const projectState = config.projects && config.projects[projectKey] ? config.projects[projectKey] : null;
28
- const shell = process.env.SHELL || '/data/data/com.termux/files/usr/bin/sh';
29
+ const interactiveShell = await resolveInteractiveShell();
29
30
  const reason = projectState && projectState.preferSafeInstall
30
31
  ? 'Este proyecto ya quedo marcado para modo seguro.'
31
32
  : 'Entrando al entorno seguro manual de Dex.';
@@ -47,13 +48,14 @@ export async function runSafeShellCommand() {
47
48
  console.log('Proyecto : ' + projectRoot);
48
49
  console.log('Entorno : ' + venvDir);
49
50
  console.log('Python : ' + rescuePython);
51
+ console.log('Shell : ' + interactiveShell.shellName + ' interactiva');
50
52
  console.log('Nota : ' + reason);
51
53
  console.log('Usa python, pip o python bot.py desde aqui.');
52
54
  console.log('Si sales de esta shell, el modo seguro deja de estar activo.');
53
55
  console.log('Escribe exit para volver a tu shell normal.');
54
56
  console.log('');
55
57
 
56
- await openSafeShell(shell, projectRoot, {
58
+ await openSafeShell(interactiveShell, projectRoot, {
57
59
  PATH: nextPath,
58
60
  VIRTUAL_ENV: venvDir,
59
61
  DEX_CONTEXT: 'safe-shell',
@@ -82,13 +84,14 @@ export async function runSafeShellCommand() {
82
84
  console.log('Proyecto : ' + projectRoot);
83
85
  console.log('Seguro : ' + safeProjectDir);
84
86
  console.log('Modulos : ' + nodeModulesDir);
87
+ console.log('Shell : ' + interactiveShell.shellName + ' interactiva');
85
88
  console.log('Nota : ' + reason);
86
89
  console.log('Usa node, npm o npm run desde aqui.');
87
90
  console.log('Esta shell abre la copia segura del proyecto dentro de HOME.');
88
91
  console.log('Escribe exit para volver a tu shell normal.');
89
92
  console.log('');
90
93
 
91
- await openSafeShell(shell, safeProjectDir, {
94
+ await openSafeShell(interactiveShell, safeProjectDir, {
92
95
  PATH: nextPath,
93
96
  DEX_CONTEXT: 'safe-shell',
94
97
  DEX_SAFE_PROJECT: projectRoot,
@@ -118,6 +121,7 @@ export async function runSafeShellCommand() {
118
121
  console.log('Proyecto : ' + projectRoot);
119
122
  console.log('Seguro : ' + safeProjectDir);
120
123
  console.log('Vendor : ' + vendorDir);
124
+ console.log('Shell : ' + interactiveShell.shellName + ' interactiva');
121
125
  console.log('Nota : ' + reason);
122
126
  console.log('Usa php, composer o binarios de vendor/bin desde aqui.');
123
127
  console.log('Esta shell abre la copia segura del proyecto dentro de HOME.');
@@ -126,7 +130,7 @@ export async function runSafeShellCommand() {
126
130
 
127
131
  await fs.mkdir(composerHome, { recursive: true });
128
132
 
129
- await openSafeShell(shell, safeProjectDir, {
133
+ await openSafeShell(interactiveShell, safeProjectDir, {
130
134
  PATH: nextPath,
131
135
  COMPOSER_HOME: composerHome,
132
136
  DEX_CONTEXT: 'safe-shell',
@@ -171,9 +175,9 @@ function supportsSafeMode(projectType) {
171
175
  return projectType === 'python' || projectType === 'node' || projectType === 'php';
172
176
  }
173
177
 
174
- function openSafeShell(shell, cwd, envPatch) {
178
+ function openSafeShell(interactiveShell, cwd, envPatch) {
175
179
  return new Promise((resolve, reject) => {
176
- const child = spawn(shell, ['-i'], {
180
+ const child = spawn(interactiveShell.shellPath, interactiveShell.shellArgs, {
177
181
  cwd,
178
182
  stdio: 'inherit',
179
183
  env: {
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const PACKAGE_NAME = 'dex-termux-cli';
6
+ const PACKAGE_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
7
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
8
+
9
+ export async function runVersionCommand() {
10
+ const localPackage = await readLocalPackage();
11
+ const localVersion = localPackage.version || 'desconocida';
12
+
13
+ console.log('Dex Version');
14
+ console.log('');
15
+ console.log('Paquete : ' + (localPackage.name || PACKAGE_NAME));
16
+ console.log('Local : ' + localVersion);
17
+
18
+ const latest = await readLatestPublishedVersion();
19
+
20
+ if (!latest) {
21
+ console.log('npm : no pude consultar la version publicada ahora mismo');
22
+ return;
23
+ }
24
+
25
+ console.log('npm : ' + latest.version);
26
+
27
+ const comparison = compareVersions(localVersion, latest.version);
28
+
29
+ if (comparison < 0) {
30
+ console.log('Estado : hay una version mas nueva publicada');
31
+ console.log('Accion : npm i -g ' + PACKAGE_NAME);
32
+ return;
33
+ }
34
+
35
+ if (comparison > 0) {
36
+ console.log('Estado : esta copia local va por delante de npm');
37
+ return;
38
+ }
39
+
40
+ console.log('Estado : local y npm estan alineados');
41
+ }
42
+
43
+ async function readLocalPackage() {
44
+ const raw = await fs.readFile(PACKAGE_PATH, 'utf8');
45
+ return JSON.parse(raw);
46
+ }
47
+
48
+ async function readLatestPublishedVersion() {
49
+ try {
50
+ const controller = new AbortController();
51
+ const timeout = setTimeout(() => controller.abort(), 2500);
52
+ const response = await fetch(REGISTRY_URL, {
53
+ headers: { accept: 'application/json' },
54
+ signal: controller.signal,
55
+ });
56
+
57
+ clearTimeout(timeout);
58
+
59
+ if (!response.ok) {
60
+ return null;
61
+ }
62
+
63
+ const data = await response.json();
64
+ return {
65
+ version: data.version || 'desconocida',
66
+ };
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
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);
76
+
77
+ for (let index = 0; index < maxLength; index += 1) {
78
+ const leftValue = leftParts[index] || 0;
79
+ const rightValue = rightParts[index] || 0;
80
+
81
+ if (leftValue > rightValue) {
82
+ return 1;
83
+ }
84
+
85
+ if (leftValue < rightValue) {
86
+ return -1;
87
+ }
88
+ }
89
+
90
+ return 0;
91
+ }
92
+
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));
98
+ }
package/src/core/args.js CHANGED
@@ -17,6 +17,21 @@ export function parseArgs(argv) {
17
17
  continue;
18
18
  }
19
19
 
20
+ if (token === '-v' || token === '--version' || token === 'version') {
21
+ parsed.command = 'version';
22
+ continue;
23
+ }
24
+
25
+ if (token === '-c' || token === '--context' || token === 'context') {
26
+ parsed.command = 'context';
27
+ continue;
28
+ }
29
+
30
+ if (token === '--prompt-context') {
31
+ parsed.command = 'prompt-context';
32
+ continue;
33
+ }
34
+
20
35
  if (token === '-m' || token === '--menu' || token === 'menu') {
21
36
  parsed.command = 'menu';
22
37
  continue;
@@ -7,6 +7,9 @@ const DEFAULT_CONFIG = {
7
7
  projectBadge: false,
8
8
  smartProjectInstall: false,
9
9
  },
10
+ ui: {
11
+ promptContextPosition: 'right',
12
+ },
10
13
  projects: {},
11
14
  };
12
15
 
@@ -55,6 +58,19 @@ export async function setFeatureEnabled(featureKey, enabled) {
55
58
  return saveUserConfig(nextConfig);
56
59
  }
57
60
 
61
+ export async function setPromptContextPosition(position) {
62
+ const config = await loadUserConfig();
63
+ const nextConfig = {
64
+ ...config,
65
+ ui: {
66
+ ...config.ui,
67
+ promptContextPosition: normalizePromptContextPosition(position),
68
+ },
69
+ };
70
+
71
+ return saveUserConfig(nextConfig);
72
+ }
73
+
58
74
  export async function saveProjectState(projectKey, projectState) {
59
75
  const config = await loadUserConfig();
60
76
  const nextConfig = {
@@ -103,6 +119,11 @@ function mergeConfig(config) {
103
119
 
104
120
  return {
105
121
  features,
122
+ ui: {
123
+ ...DEFAULT_CONFIG.ui,
124
+ ...(config.ui || {}),
125
+ promptContextPosition: normalizePromptContextPosition(config.ui?.promptContextPosition),
126
+ },
106
127
  projects: {
107
128
  ...DEFAULT_CONFIG.projects,
108
129
  ...(config.projects || {}),
@@ -115,8 +136,15 @@ function cloneDefaultConfig() {
115
136
  features: {
116
137
  ...DEFAULT_CONFIG.features,
117
138
  },
139
+ ui: {
140
+ ...DEFAULT_CONFIG.ui,
141
+ },
118
142
  projects: {
119
143
  ...DEFAULT_CONFIG.projects,
120
144
  },
121
145
  };
122
146
  }
147
+
148
+ function normalizePromptContextPosition(position) {
149
+ return ['right', 'inline', 'off'].includes(position) ? position : DEFAULT_CONFIG.ui.promptContextPosition;
150
+ }
package/src/ui/output.js CHANGED
@@ -1,68 +1,62 @@
1
1
  import path from 'node:path';
2
2
  import { getUserConfigPath } from '../core/config.js';
3
3
 
4
+ const ANSI = {
5
+ bold: '\x1b[1m',
6
+ cyan: '\x1b[36m',
7
+ dim: '\x1b[2m',
8
+ green: '\x1b[32m',
9
+ reset: '\x1b[0m',
10
+ };
11
+
4
12
  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');
13
+ printBanner();
42
14
  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');
15
+ console.log(' Explorar, entender y preparar proyectos desde Termux y Android storage.');
47
16
  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.');
17
+
18
+ printBlock('Comandos principales', [
19
+ 'dex -m menu visual',
20
+ 'dex -v version local y publicada',
21
+ 'dex -c contexto del proyecto actual',
22
+ 'dex -b "archivo" buscar archivos o carpetas',
23
+ 'dex -e ls explicar un comando',
24
+ 'dex -t src arbol guiado de una ruta',
25
+ 'dex -i instalar dependencias del proyecto',
26
+ 'dex -r abrir modo seguro del proyecto',
27
+ 'dex -a acceso rapido a Android',
28
+ ]);
29
+
30
+ printBlock('Uso rapido', [
31
+ 'dex -b "*.js" --scope actual',
32
+ 'dex -t . --depth 3',
33
+ 'dex -c',
34
+ 'dex --prompt-context',
35
+ ]);
36
+
37
+ printBlock('Scopes', [
38
+ 'actual carpeta donde estas parado',
39
+ 'home tu HOME de Termux',
40
+ 'android almacenamiento accesible',
41
+ ]);
42
+
43
+ printBlock('Atajos', [
44
+ '-h --help ayuda',
45
+ '-v --version version',
46
+ '-c --context contexto',
47
+ '-m --menu menu',
48
+ '-b --buscar buscar',
49
+ '-e --explicar explicar',
50
+ '-t --tree arbol',
51
+ '-i --instalar instalar',
52
+ '-r --seguro shell segura',
53
+ '-a --android Android',
54
+ '-s --scope scope',
55
+ '-d --depth profundidad',
56
+ ]);
57
+
58
+ console.log(dim('Configuracion: ' + getUserConfigPath()));
59
+ console.log(dim('Nota: si no pasas --scope en busqueda o tree, Dex te pregunta antes.'));
66
60
  }
67
61
 
68
62
  export function printSection(title) {
@@ -127,3 +121,32 @@ export function formatTreeReport(report) {
127
121
 
128
122
  return lines.join('\n');
129
123
  }
124
+
125
+ function printBanner() {
126
+ if (!process.stdout.isTTY) {
127
+ console.log('Dex CLI');
128
+ return;
129
+ }
130
+
131
+ console.log(ANSI.bold + ANSI.cyan + 'Dex CLI' + ANSI.reset);
132
+ console.log(dim('────────────────────────────────────────'));
133
+ }
134
+
135
+ function printBlock(title, lines) {
136
+ const heading = process.stdout.isTTY ? ANSI.bold + ANSI.green + '[' + title + ']' + ANSI.reset : '[' + title + ']';
137
+ console.log(heading);
138
+
139
+ for (const line of lines) {
140
+ console.log(' ' + line);
141
+ }
142
+
143
+ console.log('');
144
+ }
145
+
146
+ function dim(text) {
147
+ if (!process.stdout.isTTY) {
148
+ return text;
149
+ }
150
+
151
+ return ANSI.dim + text + ANSI.reset;
152
+ }
package/src/ui/prompt.js CHANGED
@@ -4,14 +4,18 @@ import { stdin as input, stdout as output } from 'node:process';
4
4
 
5
5
  const ANSI = {
6
6
  clearDown: '\x1B[0J',
7
+ cyan: '\x1B[36m',
8
+ dim: '\x1B[2m',
7
9
  green: '\x1B[32m',
8
10
  red: '\x1B[31m',
9
11
  reset: '\x1B[0m',
12
+ yellow: '\x1B[33m',
13
+ bold: '\x1B[1m',
10
14
  };
11
15
 
12
16
  export async function chooseSearchScope(options) {
13
17
  return chooseNumericOption({
14
- title: 'Elige donde quieres buscar:',
18
+ title: 'Elige donde quieres buscar',
15
19
  options,
16
20
  getValue: (option) => option.key,
17
21
  getLines: (option) => [option.label, option.description, option.root],
@@ -25,7 +29,7 @@ export async function chooseExplainTopic(entries) {
25
29
  const topics = Object.keys(entries).map((topic) => ({ topic }));
26
30
 
27
31
  return chooseNumericOption({
28
- title: 'Elige un comando para explicar:',
32
+ title: 'Elige un comando para explicar',
29
33
  options: topics,
30
34
  getValue: (option) => option.topic,
31
35
  getLines: (option) => [option.topic],
@@ -37,23 +41,27 @@ export async function chooseExplainTopic(entries) {
37
41
 
38
42
  export async function chooseMenuAction() {
39
43
  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' },
44
+ { key: 'search', label: 'Buscar archivos o carpetas', hint: 'encuentra rapido por nombre o patron' },
45
+ { key: 'explain', label: 'Explicar un comando', hint: 'resume uso, riesgo y notas' },
46
+ { key: 'tree', label: 'Ver arbol de una ruta', hint: 'muestra carpetas y archivos con profundidad guiada' },
47
+ { key: 'safe-shell', label: 'Entrar al modo seguro', hint: 'abre el proyecto con el entorno correcto' },
48
+ { key: 'version', label: 'Ver version de Dex', hint: 'compara local contra la version publicada' },
49
+ { key: 'context', label: 'Ver contexto del proyecto', hint: 'detecta el lenguaje del proyecto actual' },
50
+ { key: 'settings', label: 'Ajustes y funciones', hint: 'activa extras y mueve el badge del prompt' },
51
+ { key: 'help', label: 'Ver ayuda', hint: 'muestra comandos y ejemplos' },
52
+ { key: 'exit', label: 'Salir', hint: 'cerrar el menu' },
47
53
  ];
48
54
 
49
55
  return chooseNumericOption({
50
56
  title: 'Dex Menu',
51
57
  options,
52
58
  getValue: (option) => option.key,
53
- getLines: (option) => [option.label],
59
+ getLines: (option) => [option.label, option.hint],
54
60
  prompt: 'Opcion [1-' + options.length + ', Enter=1]: ',
55
- errorMessage: 'Opcion no valida. Usa un numero entre 1 y 7.',
61
+ errorMessage: 'Opcion no valida. Usa un numero entre 1 y 9.',
56
62
  directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
63
+ style: 'card',
64
+ introLines: ['Centro rapido para navegar, revisar version y entrar al modo seguro.'],
57
65
  });
58
66
  }
59
67
 
@@ -69,7 +77,13 @@ export async function chooseSettingsAction(config) {
69
77
  key: 'toggle-project-badge',
70
78
  label: 'Detector de proyecto y color',
71
79
  status: formatStatus(config.features.projectBadge),
72
- usage: 'muestra lenguaje y color del proyecto',
80
+ usage: 'muestra lenguaje y color dentro de Dex',
81
+ },
82
+ {
83
+ key: 'prompt-context-position',
84
+ label: 'Posicion del contexto del prompt',
85
+ status: formatPromptPosition(config.ui?.promptContextPosition || 'right'),
86
+ usage: 'elige derecha, inline junto a la ruta o desactivado',
73
87
  },
74
88
  {
75
89
  key: 'toggle-smart-project-install',
@@ -99,9 +113,55 @@ export async function chooseSettingsAction(config) {
99
113
  ];
100
114
  },
101
115
  prompt: 'Ajuste [1-' + options.length + ', Enter=1]: ',
116
+ errorMessage: 'Opcion no valida. Usa 1, 2, 3, 4 o 5.',
117
+ directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
118
+ style: 'card',
119
+ introLines: ['Activa solo lo que quieras ver todos los dias.'],
120
+ });
121
+ }
122
+
123
+ export async function choosePromptContextPosition(currentPosition) {
124
+ const options = [
125
+ {
126
+ key: 'right',
127
+ label: 'Derecha del prompt',
128
+ usage: 'muestra [PYTHON] o [NODE] en la esquina derecha',
129
+ },
130
+ {
131
+ key: 'inline',
132
+ label: 'Inline junto a la ruta',
133
+ usage: 'muestra Dex@android /ruta [PYTHON] en la misma linea',
134
+ },
135
+ {
136
+ key: 'off',
137
+ label: 'Ocultar contexto',
138
+ usage: 'no muestra badge del proyecto en el prompt',
139
+ },
140
+ {
141
+ key: 'back',
142
+ label: 'Volver',
143
+ },
144
+ ];
145
+
146
+ return chooseNumericOption({
147
+ title: 'Posicion del contexto del prompt',
148
+ options,
149
+ getValue: (option) => option.key,
150
+ getLines: (option) => {
151
+ if (option.key === 'back') {
152
+ return [option.label];
153
+ }
154
+
155
+ return [
156
+ option.label,
157
+ 'Uso : ' + option.usage,
158
+ ];
159
+ },
160
+ prompt: 'Posicion [1-' + options.length + ', Enter=1]: ',
102
161
  errorMessage: 'Opcion no valida. Usa 1, 2, 3 o 4.',
103
162
  directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
104
163
  style: 'card',
164
+ introLines: ['Actual: ' + formatPromptPosition(currentPosition)],
105
165
  });
106
166
  }
107
167
 
@@ -202,7 +262,7 @@ async function chooseNumericOption({
202
262
  return selectedValue;
203
263
  }
204
264
 
205
- console.log(errorMessage);
265
+ console.log(colorize(errorMessage, 'red'));
206
266
  }
207
267
  } finally {
208
268
  rl.close();
@@ -210,11 +270,12 @@ async function chooseNumericOption({
210
270
  }
211
271
 
212
272
  function printOptionBlock(title, options, getLines, style, introLines) {
213
- console.log(title);
273
+ console.log(colorize(title, 'cyan', 'bold'));
274
+ console.log(colorize('────────────────────────────────────────', 'dim'));
214
275
  console.log('');
215
276
 
216
277
  for (const line of introLines) {
217
- console.log(line);
278
+ console.log(colorize(line, 'dim'));
218
279
  }
219
280
 
220
281
  if (introLines.length) {
@@ -225,10 +286,10 @@ function printOptionBlock(title, options, getLines, style, introLines) {
225
286
  const lines = getLines(option);
226
287
 
227
288
  if (style === 'card') {
228
- console.log('[' + (index + 1) + '] ' + lines[0]);
289
+ console.log(colorize('[' + (index + 1) + ']', 'yellow', 'bold') + ' ' + lines[0]);
229
290
 
230
291
  for (let lineIndex = 1; lineIndex < lines.length; lineIndex += 1) {
231
- console.log(' - ' + lines[lineIndex]);
292
+ console.log(' ' + colorize('•', 'dim') + ' ' + lines[lineIndex]);
232
293
  }
233
294
 
234
295
  console.log('');
@@ -242,12 +303,12 @@ function printOptionBlock(title, options, getLines, style, introLines) {
242
303
  }
243
304
  });
244
305
 
245
- console.log('Tip: puedes escribir el numero o el nombre corto cuando aplique.');
306
+ console.log(colorize('Tip: puedes escribir el numero o el nombre corto cuando aplique.', 'dim'));
246
307
  console.log('');
247
308
  }
248
309
 
249
310
  function countRenderedLines(options, getLines, introLines, style) {
250
- let count = 2 + introLines.length;
311
+ let count = 3 + introLines.length;
251
312
 
252
313
  if (introLines.length) {
253
314
  count += 1;
@@ -294,6 +355,37 @@ function formatStatus(enabled) {
294
355
  return text;
295
356
  }
296
357
 
297
- const color = enabled ? ANSI.green : ANSI.red;
298
- return color + text + ANSI.reset;
358
+ const color = enabled ? 'green' : 'red';
359
+ return colorize(text, color, 'bold');
360
+ }
361
+
362
+ function formatPromptPosition(position) {
363
+ if (position === 'inline') {
364
+ return 'inline';
365
+ }
366
+
367
+ if (position === 'off') {
368
+ return 'oculto';
369
+ }
370
+
371
+ return 'derecha';
372
+ }
373
+
374
+ function colorize(text, color, weight = '') {
375
+ if (!output.isTTY) {
376
+ return text;
377
+ }
378
+
379
+ const parts = [];
380
+
381
+ if (weight && ANSI[weight]) {
382
+ parts.push(ANSI[weight]);
383
+ }
384
+
385
+ if (color && ANSI[color]) {
386
+ parts.push(ANSI[color]);
387
+ }
388
+
389
+ parts.push(text, ANSI.reset);
390
+ return parts.join('');
299
391
  }
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  const COLORS = {
5
5
  reset: '\x1b[0m',
6
6
  python: '\x1b[34m',
7
- node: '\x1b[31m',
7
+ node: '\x1b[32m',
8
8
  rust: '\x1b[33m',
9
9
  go: '\x1b[36m',
10
10
  java: '\x1b[35m',
@@ -13,6 +13,16 @@ const COLORS = {
13
13
  generic: '\x1b[92m',
14
14
  };
15
15
 
16
+ const LANGUAGE_META = {
17
+ python: { label: 'PYTHON' },
18
+ node: { label: 'NODE' },
19
+ rust: { label: 'RUST' },
20
+ go: { label: 'GO' },
21
+ java: { label: 'JAVA' },
22
+ php: { label: 'PHP' },
23
+ ruby: { label: 'RUBY' },
24
+ };
25
+
16
26
  export async function detectProjectContext(cwd = process.cwd()) {
17
27
  const entries = await safeReadDir(cwd);
18
28
  if (!entries.length) {
@@ -26,31 +36,31 @@ export async function detectProjectContext(cwd = process.cwd()) {
26
36
  const hasRubyFiles = entries.some((entry) => !entry.isDirectory() && entry.name.endsWith('.rb'));
27
37
 
28
38
  if (names.has('pyproject.toml') || names.has('requirements.txt') || names.has('Pipfile') || names.has('setup.py') || hasPyFiles) {
29
- return createContext('python', cwd, 'PYTHON');
39
+ return createContext('python', cwd, await detectPythonVersion(cwd));
30
40
  }
31
41
 
32
42
  if (names.has('package.json') || names.has('node_modules') || hasJsFiles) {
33
- return createContext('node', cwd, 'NODE');
43
+ return createContext('node', cwd, await detectNodeVersion(cwd));
34
44
  }
35
45
 
36
46
  if (names.has('Cargo.toml')) {
37
- return createContext('rust', cwd, 'RUST');
47
+ return createContext('rust', cwd, await detectRustVersion(cwd));
38
48
  }
39
49
 
40
50
  if (names.has('go.mod')) {
41
- return createContext('go', cwd, 'GO');
51
+ return createContext('go', cwd, await detectGoVersion(cwd));
42
52
  }
43
53
 
44
54
  if (names.has('pom.xml') || names.has('build.gradle') || names.has('build.gradle.kts')) {
45
- return createContext('java', cwd, 'JAVA');
55
+ return createContext('java', cwd, await detectJavaVersion(cwd));
46
56
  }
47
57
 
48
58
  if (names.has('composer.json') || hasPhpFiles) {
49
- return createContext('php', cwd, 'PHP');
59
+ return createContext('php', cwd, await detectPhpVersion(cwd));
50
60
  }
51
61
 
52
62
  if (names.has('Gemfile') || hasRubyFiles) {
53
- return createContext('ruby', cwd, 'RUBY');
63
+ return createContext('ruby', cwd, await detectRubyVersion(cwd));
54
64
  }
55
65
 
56
66
  return null;
@@ -58,15 +68,131 @@ export async function detectProjectContext(cwd = process.cwd()) {
58
68
 
59
69
  export function formatProjectContext(context) {
60
70
  const folderName = path.basename(context.cwd) || context.cwd;
71
+ const segment = buildContextSegment(context);
61
72
  const badge = process.stdout.isTTY
62
- ? `${COLORS[context.type] || COLORS.generic}[${context.label}]${COLORS.reset}`
63
- : `[${context.label}]`;
73
+ ? `${COLORS[context.type] || COLORS.generic}[${segment}]${COLORS.reset}`
74
+ : `[${segment}]`;
64
75
 
65
76
  return `Contexto: ${folderName} ${badge}`;
66
77
  }
67
78
 
68
- function createContext(type, cwd, label) {
69
- return { type, cwd, label };
79
+ export function formatPromptContext(context) {
80
+ return buildContextSegment(context);
81
+ }
82
+
83
+ export function formatProjectContextDetails(context) {
84
+ return [
85
+ 'Carpeta : ' + context.cwd,
86
+ 'Tipo : ' + context.label,
87
+ 'Version : ' + (context.version || 'sin detectar'),
88
+ 'Prompt : [' + formatPromptContext(context) + ']',
89
+ ].join('\n');
90
+ }
91
+
92
+ function createContext(type, cwd, version) {
93
+ const meta = LANGUAGE_META[type] || { label: type.toUpperCase() };
94
+ return {
95
+ type,
96
+ cwd,
97
+ label: meta.label,
98
+ version,
99
+ };
100
+ }
101
+
102
+ function buildContextSegment(context) {
103
+ return context.label;
104
+ }
105
+
106
+ async function detectPythonVersion(cwd) {
107
+ const pythonVersionFile = await readFileIfExists(path.join(cwd, '.python-version'));
108
+ const pipfile = await readFileIfExists(path.join(cwd, 'Pipfile'));
109
+ const pyproject = await readFileIfExists(path.join(cwd, 'pyproject.toml'));
110
+ const venvConfig = await readFileIfExists(path.join(cwd, '.venv', 'pyvenv.cfg'));
111
+ const runtime = await readFileIfExists(path.join(cwd, 'runtime.txt'));
112
+
113
+ return compactVersion(
114
+ firstNonEmptyLine(pythonVersionFile) ||
115
+ matchVersion(pipfile, /python_version\s*=\s*["']([^"']+)["']/i) ||
116
+ matchVersion(pyproject, /requires-python\s*=\s*["']([^"']+)["']/i) ||
117
+ matchVersion(venvConfig, /^version\s*=\s*(.+)$/im) ||
118
+ matchVersion(runtime, /python-([^\s]+)/i),
119
+ );
120
+ }
121
+
122
+ async function detectNodeVersion(cwd) {
123
+ const nvmrc = await readFileIfExists(path.join(cwd, '.nvmrc'));
124
+ const nodeVersionFile = await readFileIfExists(path.join(cwd, '.node-version'));
125
+ const packageJsonRaw = await readFileIfExists(path.join(cwd, 'package.json'));
126
+
127
+ try {
128
+ if (packageJsonRaw) {
129
+ const packageJson = JSON.parse(packageJsonRaw);
130
+ return compactVersion(
131
+ firstNonEmptyLine(nvmrc) ||
132
+ firstNonEmptyLine(nodeVersionFile) ||
133
+ packageJson.engines?.node,
134
+ );
135
+ }
136
+ } catch {
137
+ return compactVersion(firstNonEmptyLine(nvmrc) || firstNonEmptyLine(nodeVersionFile));
138
+ }
139
+
140
+ return compactVersion(firstNonEmptyLine(nvmrc) || firstNonEmptyLine(nodeVersionFile));
141
+ }
142
+
143
+ async function detectPhpVersion(cwd) {
144
+ const composerRaw = await readFileIfExists(path.join(cwd, 'composer.json'));
145
+
146
+ try {
147
+ if (composerRaw) {
148
+ const composer = JSON.parse(composerRaw);
149
+ return compactVersion(composer.require?.php);
150
+ }
151
+ } catch {
152
+ return null;
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ async function detectGoVersion(cwd) {
159
+ const goMod = await readFileIfExists(path.join(cwd, 'go.mod'));
160
+ return compactVersion(matchVersion(goMod, /^go\s+([^\s]+)$/im));
161
+ }
162
+
163
+ async function detectRustVersion(cwd) {
164
+ const cargoToml = await readFileIfExists(path.join(cwd, 'Cargo.toml'));
165
+ const toolchain = await readFileIfExists(path.join(cwd, 'rust-toolchain'));
166
+ const toolchainToml = await readFileIfExists(path.join(cwd, 'rust-toolchain.toml'));
167
+
168
+ return compactVersion(
169
+ matchVersion(cargoToml, /rust-version\s*=\s*["']([^"']+)["']/i) ||
170
+ firstNonEmptyLine(toolchain) ||
171
+ matchVersion(toolchainToml, /channel\s*=\s*["']([^"']+)["']/i),
172
+ );
173
+ }
174
+
175
+ async function detectRubyVersion(cwd) {
176
+ const rubyVersion = await readFileIfExists(path.join(cwd, '.ruby-version'));
177
+ const gemfile = await readFileIfExists(path.join(cwd, 'Gemfile'));
178
+
179
+ return compactVersion(
180
+ firstNonEmptyLine(rubyVersion) ||
181
+ matchVersion(gemfile, /ruby\s+["']([^"']+)["']/i),
182
+ );
183
+ }
184
+
185
+ async function detectJavaVersion(cwd) {
186
+ const pom = await readFileIfExists(path.join(cwd, 'pom.xml'));
187
+ const gradle = await readFileIfExists(path.join(cwd, 'build.gradle'));
188
+ const gradleKts = await readFileIfExists(path.join(cwd, 'build.gradle.kts'));
189
+
190
+ return compactVersion(
191
+ matchVersion(pom, /<java.version>([^<]+)<\/java.version>/i) ||
192
+ matchVersion(pom, /<maven.compiler.release>([^<]+)<\/maven.compiler.release>/i) ||
193
+ matchVersion(gradle, /sourceCompatibility\s*=\s*['"]?([^\s'"]+)/i) ||
194
+ matchVersion(gradleKts, /JavaLanguageVersion\.of\((\d+)\)/i),
195
+ );
70
196
  }
71
197
 
72
198
  async function safeReadDir(directoryPath) {
@@ -77,6 +203,50 @@ async function safeReadDir(directoryPath) {
77
203
  }
78
204
  }
79
205
 
206
+ async function readFileIfExists(filePath) {
207
+ try {
208
+ return await fs.readFile(filePath, 'utf8');
209
+ } catch {
210
+ return '';
211
+ }
212
+ }
213
+
214
+ function compactVersion(rawValue) {
215
+ if (!rawValue) {
216
+ return null;
217
+ }
218
+
219
+ const cleaned = String(rawValue).trim();
220
+ const versionMatch = cleaned.match(/\d+(?:\.\d+){0,2}/);
221
+
222
+ if (versionMatch) {
223
+ const parts = versionMatch[0].split('.');
224
+ return parts.slice(0, 2).join('.');
225
+ }
226
+
227
+ return cleaned.slice(0, 10);
228
+ }
229
+
230
+ function matchVersion(content, expression) {
231
+ if (!content) {
232
+ return '';
233
+ }
234
+
235
+ const match = content.match(expression);
236
+ return match?.[1]?.trim() || '';
237
+ }
238
+
239
+ function firstNonEmptyLine(content) {
240
+ if (!content) {
241
+ return '';
242
+ }
243
+
244
+ return content
245
+ .split(/\r?\n/)
246
+ .map((line) => line.trim())
247
+ .find(Boolean) || '';
248
+ }
249
+
80
250
  function isNodeScript(name) {
81
251
  return name.endsWith('.js') || name.endsWith('.mjs') || name.endsWith('.cjs') || name.endsWith('.ts');
82
252
  }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const TERMUX_ZSH = '/data/data/com.termux/files/usr/bin/zsh';
5
+ const TERMUX_BASH = '/data/data/com.termux/files/usr/bin/bash';
6
+ const TERMUX_SH = '/data/data/com.termux/files/usr/bin/sh';
7
+
8
+ export async function resolveInteractiveShell() {
9
+ const preferredShell = process.env.DEX_PREFERRED_SHELL || process.env.SHELL || '';
10
+ const candidates = buildShellCandidates(preferredShell);
11
+
12
+ for (const shellPath of candidates) {
13
+ if (!shellPath) {
14
+ continue;
15
+ }
16
+
17
+ if (await shellExists(shellPath)) {
18
+ return {
19
+ shellPath,
20
+ shellArgs: ['-i'],
21
+ shellName: path.basename(shellPath),
22
+ };
23
+ }
24
+ }
25
+
26
+ return {
27
+ shellPath: TERMUX_SH,
28
+ shellArgs: ['-i'],
29
+ shellName: 'sh',
30
+ };
31
+ }
32
+
33
+ function buildShellCandidates(preferredShell) {
34
+ const shellName = path.basename(preferredShell || '');
35
+
36
+ if (preferredShell && shellName && shellName !== 'sh') {
37
+ return dedupe([preferredShell, TERMUX_ZSH, TERMUX_BASH, TERMUX_SH]);
38
+ }
39
+
40
+ return dedupe([TERMUX_ZSH, TERMUX_BASH, preferredShell, TERMUX_SH]);
41
+ }
42
+
43
+ async function shellExists(shellPath) {
44
+ try {
45
+ await fs.access(shellPath);
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ function dedupe(values) {
53
+ return [...new Set(values.filter(Boolean))];
54
+ }