dex-termux-cli 0.2.3 → 0.3.0-beta.2

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/src/ui/prompt.js CHANGED
@@ -3,14 +3,14 @@ import { moveCursor, cursorTo } from 'node:readline';
3
3
  import { stdin as input, stdout as output } from 'node:process';
4
4
 
5
5
  const ANSI = {
6
- clearDown: '\x1B[0J',
7
- cyan: '\x1B[36m',
8
- dim: '\x1B[2m',
9
- green: '\x1B[32m',
10
- red: '\x1B[31m',
11
- reset: '\x1B[0m',
12
- yellow: '\x1B[33m',
13
- bold: '\x1B[1m',
6
+ clearDown: '',
7
+ cyan: '',
8
+ dim: '',
9
+ green: '',
10
+ red: '',
11
+ reset: '',
12
+ yellow: '',
13
+ bold: '',
14
14
  };
15
15
 
16
16
  export async function chooseSearchScope(options) {
@@ -20,7 +20,7 @@ export async function chooseSearchScope(options) {
20
20
  getValue: (option) => option.key,
21
21
  getLines: (option) => [option.label, option.description, option.root],
22
22
  prompt: 'Scope [1-' + options.length + ', Enter=1]: ',
23
- errorMessage: 'Opcion no valida. Usa un numero o actual/home/android.',
23
+ errorMessage: 'Opcion no valida. Usa un numero o una clave valida.',
24
24
  directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
25
25
  });
26
26
  }
@@ -47,7 +47,7 @@ export async function chooseMenuAction() {
47
47
  { key: 'safe-shell', label: 'Entrar al modo seguro', hint: 'abre el proyecto con el entorno correcto' },
48
48
  { key: 'version', label: 'Ver version de Dex', hint: 'compara local contra la version publicada' },
49
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' },
50
+ { key: 'settings', label: 'Ajustes y funciones', hint: 'activa extras, plataforma y prompt' },
51
51
  { key: 'help', label: 'Ver ayuda', hint: 'muestra comandos y ejemplos' },
52
52
  { key: 'exit', label: 'Salir', hint: 'cerrar el menu' },
53
53
  ];
@@ -67,11 +67,17 @@ export async function chooseMenuAction() {
67
67
 
68
68
  export async function chooseSettingsAction(config) {
69
69
  const options = [
70
+ {
71
+ key: 'platform-mode',
72
+ label: 'Modo de plataforma',
73
+ status: formatPlatformMode(config.runtime?.platformMode || 'auto'),
74
+ usage: 'elige auto, termux o linux',
75
+ },
70
76
  {
71
77
  key: 'toggle-android-shortcut',
72
- label: 'Acceso rapido a Android',
78
+ label: 'Acceso rapido',
73
79
  status: formatStatus(config.features.androidShortcut),
74
- usage: 'permite dex -a',
80
+ usage: 'permite dex -a en Termux o Linux',
75
81
  },
76
82
  {
77
83
  key: 'toggle-project-badge',
@@ -89,7 +95,7 @@ export async function chooseSettingsAction(config) {
89
95
  key: 'toggle-smart-project-install',
90
96
  label: 'Instalacion segura de proyectos',
91
97
  status: formatStatus(config.features.smartProjectInstall),
92
- usage: 'rescata instalaciones y puede instalar runtimes con pkg',
98
+ usage: 'rescata instalaciones y usa modo seguro por lenguaje',
93
99
  },
94
100
  {
95
101
  key: 'back',
@@ -113,7 +119,7 @@ export async function chooseSettingsAction(config) {
113
119
  ];
114
120
  },
115
121
  prompt: 'Ajuste [1-' + options.length + ', Enter=1]: ',
116
- errorMessage: 'Opcion no valida. Usa 1, 2, 3, 4 o 5.',
122
+ errorMessage: 'Opcion no valida. Usa 1, 2, 3, 4, 5 o 6.',
117
123
  directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
118
124
  style: 'card',
119
125
  introLines: ['Activa solo lo que quieras ver todos los dias.'],
@@ -165,6 +171,51 @@ export async function choosePromptContextPosition(currentPosition) {
165
171
  });
166
172
  }
167
173
 
174
+ export async function choosePlatformMode(currentMode) {
175
+ const options = [
176
+ {
177
+ key: 'auto',
178
+ label: 'Auto',
179
+ usage: 'detecta si estas en Termux o Linux segun el entorno real',
180
+ },
181
+ {
182
+ key: 'termux',
183
+ label: 'Modo Termux',
184
+ usage: 'activa comportamiento centrado en Android storage y Termux',
185
+ },
186
+ {
187
+ key: 'linux',
188
+ label: 'Modo Linux',
189
+ usage: 'desactiva Android y asume un entorno Linux puro',
190
+ },
191
+ {
192
+ key: 'back',
193
+ label: 'Volver',
194
+ },
195
+ ];
196
+
197
+ return chooseNumericOption({
198
+ title: 'Modo de plataforma',
199
+ options,
200
+ getValue: (option) => option.key,
201
+ getLines: (option) => {
202
+ if (option.key === 'back') {
203
+ return [option.label];
204
+ }
205
+
206
+ return [
207
+ option.label,
208
+ 'Uso : ' + option.usage,
209
+ ];
210
+ },
211
+ prompt: 'Modo [1-' + options.length + ', Enter=1]: ',
212
+ errorMessage: 'Opcion no valida. Usa 1, 2, 3 o 4.',
213
+ directMatch: (answer) => options.find((option) => option.key === answer.toLowerCase())?.key,
214
+ style: 'card',
215
+ introLines: ['Actual: ' + formatPlatformMode(currentMode)],
216
+ });
217
+ }
218
+
168
219
  export async function chooseFeatureToggle(currentValue, featureName, description) {
169
220
  const options = [
170
221
  {
@@ -371,6 +422,18 @@ function formatPromptPosition(position) {
371
422
  return 'derecha';
372
423
  }
373
424
 
425
+ function formatPlatformMode(mode) {
426
+ if (mode === 'linux') {
427
+ return 'linux';
428
+ }
429
+
430
+ if (mode === 'termux') {
431
+ return 'termux';
432
+ }
433
+
434
+ return 'auto';
435
+ }
436
+
374
437
  function colorize(text, color, weight = '') {
375
438
  if (!output.isTTY) {
376
439
  return text;
@@ -0,0 +1,116 @@
1
+ import path from 'node:path';
2
+
3
+ const TERMUX_HOME = '/data/data/com.termux/files/home';
4
+ const ANDROID_STORAGE = '/sdcard';
5
+
6
+ export function getHomeDirectory() {
7
+ return process.env.HOME || TERMUX_HOME;
8
+ }
9
+
10
+ export function isHostTermux() {
11
+ const home = getHomeDirectory();
12
+ return Boolean(process.env.TERMUX_VERSION) || home === TERMUX_HOME || home.startsWith(TERMUX_HOME + path.sep);
13
+ }
14
+
15
+ export function normalizePlatformMode(mode) {
16
+ return ['auto', 'termux', 'linux'].includes(mode) ? mode : 'auto';
17
+ }
18
+
19
+ export function resolvePlatformMode(config) {
20
+ const configured = normalizePlatformMode(config?.runtime?.platformMode || 'auto');
21
+
22
+ if (configured !== 'auto') {
23
+ return configured;
24
+ }
25
+
26
+ return isHostTermux() ? 'termux' : 'linux';
27
+ }
28
+
29
+ export function formatPlatformMode(mode) {
30
+ if (mode === 'linux') {
31
+ return 'linux';
32
+ }
33
+
34
+ if (mode === 'termux') {
35
+ return 'termux';
36
+ }
37
+
38
+ return 'auto';
39
+ }
40
+
41
+ export function getHomeScopeLabel(platformMode) {
42
+ return platformMode === 'linux' ? 'Home de Linux' : 'Home de Termux';
43
+ }
44
+
45
+ export function getHomeScopeDescription(platformMode) {
46
+ return platformMode === 'linux'
47
+ ? 'Busca dentro de tu entorno principal de Linux.'
48
+ : 'Busca dentro de tu entorno principal de Termux.';
49
+ }
50
+
51
+ export function canUseAndroidFeatures(platformMode) {
52
+ return platformMode === 'termux';
53
+ }
54
+
55
+ export function getAndroidStorageRoot(platformMode) {
56
+ return canUseAndroidFeatures(platformMode) ? ANDROID_STORAGE : '';
57
+ }
58
+
59
+ export function getQuickAccessRoot(platformMode) {
60
+ return platformMode === 'termux' ? ANDROID_STORAGE : getHomeDirectory();
61
+ }
62
+
63
+ export function getQuickAccessTitle(platformMode) {
64
+ return platformMode === 'termux' ? 'Dex Android' : 'Dex Linux';
65
+ }
66
+
67
+ export function getQuickAccessModeDescription(platformMode) {
68
+ return platformMode === 'termux'
69
+ ? 'interfaz Android redisenada'
70
+ : 'interfaz Linux redisenada';
71
+ }
72
+
73
+ export function getQuickAccessLabel(platformMode) {
74
+ return platformMode === 'termux' ? 'ANDROID STORAGE' : 'LINUX HOME';
75
+ }
76
+
77
+ export function getQuickAccessShortcutSummary(platformMode) {
78
+ return platformMode === 'termux'
79
+ ? 'dl, docs, dcim, pics, music, movies, shared'
80
+ : 'home, dl, docs, desk, pics, music, vids, tmp, shared';
81
+ }
82
+
83
+ export function getQuickAccessAliases(platformMode) {
84
+ if (platformMode === 'termux') {
85
+ return {
86
+ shared: '/sdcard',
87
+ dl: '/sdcard/Download',
88
+ docs: '/sdcard/Documents',
89
+ dcim: '/sdcard/DCIM',
90
+ pics: '/sdcard/Pictures',
91
+ music: '/sdcard/Music',
92
+ movies: '/sdcard/Movies',
93
+ };
94
+ }
95
+
96
+ const home = getHomeDirectory();
97
+ return {
98
+ home,
99
+ shared: home,
100
+ dl: path.join(home, 'Downloads'),
101
+ docs: path.join(home, 'Documents'),
102
+ desk: path.join(home, 'Desktop'),
103
+ pics: path.join(home, 'Pictures'),
104
+ music: path.join(home, 'Music'),
105
+ vids: path.join(home, 'Videos'),
106
+ tmp: '/tmp',
107
+ };
108
+ }
109
+
110
+ export function shouldRestrictProjectToAndroidStorage(platformMode) {
111
+ return platformMode === 'termux';
112
+ }
113
+
114
+ export function isAndroidStoragePath(targetPath) {
115
+ return targetPath.startsWith('/sdcard') || targetPath.startsWith('/storage/emulated/0');
116
+ }
@@ -23,8 +23,65 @@ const LANGUAGE_META = {
23
23
  ruby: { label: 'RUBY' },
24
24
  };
25
25
 
26
+ const MAX_PARENT_LOOKUP = 16;
27
+
26
28
  export async function detectProjectContext(cwd = process.cwd()) {
27
- const entries = await safeReadDir(cwd);
29
+ const searchChain = buildSearchChain(cwd);
30
+ const detections = [];
31
+
32
+ for (const directoryPath of searchChain) {
33
+ const detected = await detectProjectContextAt(directoryPath, cwd);
34
+ if (detected) {
35
+ detections.push(detected);
36
+ }
37
+ }
38
+
39
+ if (!detections.length) {
40
+ return null;
41
+ }
42
+
43
+ const explicit = detections.filter((item) => item.detectionKind === 'explicit');
44
+ if (explicit.length) {
45
+ return pickBestDetection(explicit);
46
+ }
47
+
48
+ return pickBestDetection(detections);
49
+ }
50
+
51
+ export function formatProjectContext(context) {
52
+ const folderName = path.basename(context.projectRoot || context.cwd) || context.cwd;
53
+ const segment = buildContextSegment(context);
54
+ const badge = process.stdout.isTTY
55
+ ? `${COLORS[context.type] || COLORS.generic}[${segment}]${COLORS.reset}`
56
+ : `[${segment}]`;
57
+
58
+ return `Contexto: ${folderName} ${badge}`;
59
+ }
60
+
61
+ export function formatPromptContext(context) {
62
+ return buildContextSegment(context);
63
+ }
64
+
65
+ export function formatPromptProjectRoot(context) {
66
+ return context.projectRoot || context.cwd;
67
+ }
68
+
69
+ export function formatPromptProjectPath(context) {
70
+ return context.projectRoot || context.cwd;
71
+ }
72
+
73
+ export function formatProjectContextDetails(context) {
74
+ return [
75
+ 'Carpeta : ' + context.cwd,
76
+ 'Proyecto: ' + (context.projectRoot || context.cwd),
77
+ 'Tipo : ' + context.label,
78
+ 'Version : ' + (context.version || 'sin detectar'),
79
+ 'Prompt : [' + formatPromptContext(context) + ']',
80
+ ].join('\n');
81
+ }
82
+
83
+ async function detectProjectContextAt(targetDir, cwd) {
84
+ const entries = await safeReadDir(targetDir);
28
85
  if (!entries.length) {
29
86
  return null;
30
87
  }
@@ -35,67 +92,114 @@ export async function detectProjectContext(cwd = process.cwd()) {
35
92
  const hasPhpFiles = entries.some((entry) => !entry.isDirectory() && entry.name.endsWith('.php'));
36
93
  const hasRubyFiles = entries.some((entry) => !entry.isDirectory() && entry.name.endsWith('.rb'));
37
94
 
38
- if (names.has('pyproject.toml') || names.has('requirements.txt') || names.has('Pipfile') || names.has('setup.py') || hasPyFiles) {
39
- return createContext('python', cwd, await detectPythonVersion(cwd));
95
+ if (names.has('pyproject.toml') || names.has('requirements.txt') || names.has('Pipfile') || names.has('setup.py')) {
96
+ return createContext('python', cwd, targetDir, await detectPythonVersion(targetDir), 'explicit');
40
97
  }
41
98
 
42
- if (names.has('package.json') || names.has('node_modules') || hasJsFiles) {
43
- return createContext('node', cwd, await detectNodeVersion(cwd));
99
+ if (names.has('package.json') || names.has('node_modules')) {
100
+ return createContext('node', cwd, targetDir, await detectNodeVersion(targetDir), 'explicit');
44
101
  }
45
102
 
46
103
  if (names.has('Cargo.toml')) {
47
- return createContext('rust', cwd, await detectRustVersion(cwd));
104
+ return createContext('rust', cwd, targetDir, await detectRustVersion(targetDir), 'explicit');
48
105
  }
49
106
 
50
107
  if (names.has('go.mod')) {
51
- return createContext('go', cwd, await detectGoVersion(cwd));
108
+ return createContext('go', cwd, targetDir, await detectGoVersion(targetDir), 'explicit');
52
109
  }
53
110
 
54
111
  if (names.has('pom.xml') || names.has('build.gradle') || names.has('build.gradle.kts')) {
55
- return createContext('java', cwd, await detectJavaVersion(cwd));
112
+ return createContext('java', cwd, targetDir, await detectJavaVersion(targetDir), 'explicit');
113
+ }
114
+
115
+ if (names.has('composer.json')) {
116
+ return createContext('php', cwd, targetDir, await detectPhpVersion(targetDir), 'explicit');
117
+ }
118
+
119
+ if (names.has('Gemfile')) {
120
+ return createContext('ruby', cwd, targetDir, await detectRubyVersion(targetDir), 'explicit');
121
+ }
122
+
123
+ if (hasPyFiles) {
124
+ return createContext('python', cwd, targetDir, await detectPythonVersion(targetDir), 'implicit');
125
+ }
126
+
127
+ if (hasJsFiles) {
128
+ return createContext('node', cwd, targetDir, await detectNodeVersion(targetDir), 'implicit');
56
129
  }
57
130
 
58
- if (names.has('composer.json') || hasPhpFiles) {
59
- return createContext('php', cwd, await detectPhpVersion(cwd));
131
+ if (hasPhpFiles) {
132
+ return createContext('php', cwd, targetDir, await detectPhpVersion(targetDir), 'implicit');
60
133
  }
61
134
 
62
- if (names.has('Gemfile') || hasRubyFiles) {
63
- return createContext('ruby', cwd, await detectRubyVersion(cwd));
135
+ if (hasRubyFiles) {
136
+ return createContext('ruby', cwd, targetDir, await detectRubyVersion(targetDir), 'implicit');
137
+ }
138
+
139
+ const nestedImplicitType = await detectImplicitLanguageFromChildren(targetDir, entries);
140
+ if (nestedImplicitType) {
141
+ return createContext(nestedImplicitType, cwd, targetDir, await detectVersionForType(nestedImplicitType, targetDir), 'implicit');
64
142
  }
65
143
 
66
144
  return null;
67
145
  }
68
146
 
69
- export function formatProjectContext(context) {
70
- const folderName = path.basename(context.cwd) || context.cwd;
71
- const segment = buildContextSegment(context);
72
- const badge = process.stdout.isTTY
73
- ? `${COLORS[context.type] || COLORS.generic}[${segment}]${COLORS.reset}`
74
- : `[${segment}]`;
147
+ async function detectImplicitLanguageFromChildren(targetDir, entries) {
148
+ const childDirectories = entries
149
+ .filter((entry) => entry.isDirectory())
150
+ .slice(0, 8);
75
151
 
76
- return `Contexto: ${folderName} ${badge}`;
77
- }
152
+ for (const entry of childDirectories) {
153
+ const nestedEntries = await safeReadDir(path.join(targetDir, entry.name));
154
+ if (!nestedEntries.length) {
155
+ continue;
156
+ }
78
157
 
79
- export function formatPromptContext(context) {
80
- return buildContextSegment(context);
158
+ if (nestedEntries.some((item) => !item.isDirectory() && item.name.endsWith('.py'))) {
159
+ return 'python';
160
+ }
161
+
162
+ if (nestedEntries.some((item) => !item.isDirectory() && isNodeScript(item.name))) {
163
+ return 'node';
164
+ }
165
+
166
+ if (nestedEntries.some((item) => !item.isDirectory() && item.name.endsWith('.php'))) {
167
+ return 'php';
168
+ }
169
+
170
+ if (nestedEntries.some((item) => !item.isDirectory() && item.name.endsWith('.rb'))) {
171
+ return 'ruby';
172
+ }
173
+ }
174
+
175
+ return '';
81
176
  }
82
177
 
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');
178
+ function pickBestDetection(detections) {
179
+ let bestMatch = detections[0];
180
+
181
+ for (let index = 1; index < detections.length; index += 1) {
182
+ const current = detections[index];
183
+
184
+ if (current.type !== bestMatch.type) {
185
+ break;
186
+ }
187
+
188
+ bestMatch = current;
189
+ }
190
+
191
+ return bestMatch;
90
192
  }
91
193
 
92
- function createContext(type, cwd, version) {
194
+ function createContext(type, cwd, projectRoot, version, detectionKind) {
93
195
  const meta = LANGUAGE_META[type] || { label: type.toUpperCase() };
94
196
  return {
95
197
  type,
96
198
  cwd,
199
+ projectRoot,
97
200
  label: meta.label,
98
201
  version,
202
+ detectionKind,
99
203
  };
100
204
  }
101
205
 
@@ -103,6 +207,22 @@ function buildContextSegment(context) {
103
207
  return context.label;
104
208
  }
105
209
 
210
+ function buildSearchChain(cwd) {
211
+ const chain = [];
212
+ let current = path.resolve(cwd);
213
+
214
+ for (let depth = 0; depth < MAX_PARENT_LOOKUP; depth += 1) {
215
+ chain.push(current);
216
+ const parent = path.dirname(current);
217
+ if (parent === current) {
218
+ break;
219
+ }
220
+ current = parent;
221
+ }
222
+
223
+ return chain;
224
+ }
225
+
106
226
  async function detectPythonVersion(cwd) {
107
227
  const pythonVersionFile = await readFileIfExists(path.join(cwd, '.python-version'));
108
228
  const pipfile = await readFileIfExists(path.join(cwd, 'Pipfile'));
@@ -190,11 +310,43 @@ async function detectJavaVersion(cwd) {
190
310
  return compactVersion(
191
311
  matchVersion(pom, /<java.version>([^<]+)<\/java.version>/i) ||
192
312
  matchVersion(pom, /<maven.compiler.release>([^<]+)<\/maven.compiler.release>/i) ||
193
- matchVersion(gradle, /sourceCompatibility\s*=\s*['"]?([^\s'"]+)/i) ||
313
+ matchVersion(gradle, /sourceCompatibility\s*=\s*['\"]?([^\s'\"]+)/i) ||
194
314
  matchVersion(gradleKts, /JavaLanguageVersion\.of\((\d+)\)/i),
195
315
  );
196
316
  }
197
317
 
318
+ async function detectVersionForType(type, cwd) {
319
+ if (type === 'python') {
320
+ return detectPythonVersion(cwd);
321
+ }
322
+
323
+ if (type === 'node') {
324
+ return detectNodeVersion(cwd);
325
+ }
326
+
327
+ if (type === 'php') {
328
+ return detectPhpVersion(cwd);
329
+ }
330
+
331
+ if (type === 'ruby') {
332
+ return detectRubyVersion(cwd);
333
+ }
334
+
335
+ if (type === 'go') {
336
+ return detectGoVersion(cwd);
337
+ }
338
+
339
+ if (type === 'rust') {
340
+ return detectRustVersion(cwd);
341
+ }
342
+
343
+ if (type === 'java') {
344
+ return detectJavaVersion(cwd);
345
+ }
346
+
347
+ return null;
348
+ }
349
+
198
350
  async function safeReadDir(directoryPath) {
199
351
  try {
200
352
  return await fs.readdir(directoryPath, { withFileTypes: true });
@@ -1,13 +1,17 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { isHostTermux } from './platform.js';
3
4
 
4
5
  const TERMUX_ZSH = '/data/data/com.termux/files/usr/bin/zsh';
5
6
  const TERMUX_BASH = '/data/data/com.termux/files/usr/bin/bash';
6
7
  const TERMUX_SH = '/data/data/com.termux/files/usr/bin/sh';
8
+ const LINUX_ZSH = '/bin/zsh';
9
+ const LINUX_BASH = '/bin/bash';
10
+ const LINUX_SH = '/bin/sh';
7
11
 
8
- export async function resolveInteractiveShell() {
12
+ export async function resolveInteractiveShell(platformMode = '') {
9
13
  const preferredShell = process.env.DEX_PREFERRED_SHELL || process.env.SHELL || '';
10
- const candidates = buildShellCandidates(preferredShell);
14
+ const candidates = buildShellCandidates(preferredShell, platformMode);
11
15
 
12
16
  for (const shellPath of candidates) {
13
17
  if (!shellPath) {
@@ -24,20 +28,42 @@ export async function resolveInteractiveShell() {
24
28
  }
25
29
 
26
30
  return {
27
- shellPath: TERMUX_SH,
31
+ shellPath: LINUX_SH,
28
32
  shellArgs: ['-i'],
29
33
  shellName: 'sh',
30
34
  };
31
35
  }
32
36
 
33
- function buildShellCandidates(preferredShell) {
37
+ function buildShellCandidates(preferredShell, platformMode = '') {
34
38
  const shellName = path.basename(preferredShell || '');
39
+ const preferredFirst = preferredShell && shellName && shellName !== 'sh'
40
+ ? [preferredShell]
41
+ : [];
42
+ const preferTermuxShells = platformMode ? platformMode !== 'linux' : isHostTermux();
35
43
 
36
- if (preferredShell && shellName && shellName !== 'sh') {
37
- return dedupe([preferredShell, TERMUX_ZSH, TERMUX_BASH, TERMUX_SH]);
44
+ if (preferTermuxShells) {
45
+ return dedupe([
46
+ ...preferredFirst,
47
+ TERMUX_ZSH,
48
+ TERMUX_BASH,
49
+ LINUX_ZSH,
50
+ LINUX_BASH,
51
+ preferredShell,
52
+ TERMUX_SH,
53
+ LINUX_SH,
54
+ ]);
38
55
  }
39
56
 
40
- return dedupe([TERMUX_ZSH, TERMUX_BASH, preferredShell, TERMUX_SH]);
57
+ return dedupe([
58
+ ...preferredFirst,
59
+ LINUX_ZSH,
60
+ LINUX_BASH,
61
+ preferredShell,
62
+ LINUX_SH,
63
+ TERMUX_ZSH,
64
+ TERMUX_BASH,
65
+ TERMUX_SH,
66
+ ]);
41
67
  }
42
68
 
43
69
  async function shellExists(shellPath) {