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

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.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/app/main.js';
3
+
4
+ const argv = process.argv.slice(2);
5
+
6
+ if (argv.includes('--root')) {
7
+ await main(['--prompt-project-root']);
8
+ } else if (argv.includes('--path')) {
9
+ await main(['--prompt-project-path']);
10
+ } else {
11
+ await main(['--prompt-context']);
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dex-termux-cli",
3
- "version": "0.3.0-beta.2",
3
+ "version": "0.3.0-beta.4",
4
4
  "type": "module",
5
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": [
@@ -19,7 +19,8 @@
19
19
  ],
20
20
  "license": "MIT",
21
21
  "bin": {
22
- "dex": "bin/dex"
22
+ "dex": "bin/dex",
23
+ "dex-project-context": "bin/dex-project-context"
23
24
  },
24
25
  "scripts": {
25
26
  "start": "node ./bin/dex",
@@ -29,6 +30,7 @@
29
30
  "author": "farllirs",
30
31
  "files": [
31
32
  "bin/dex",
33
+ "bin/dex-project-context",
32
34
  "src",
33
35
  "data/commands/explain.json",
34
36
  "README.md",
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
  }
@@ -197,18 +197,50 @@ function buildLinuxShellRc({ aliases, root, label, shellName }) {
197
197
  return [
198
198
  ...commonLines,
199
199
  '[[ -f "$HOME/.zshrc" ]] && source "$HOME/.zshrc"',
200
+ '_dex_linux_project_root_zsh() {',
201
+ ' local helper',
202
+ ' helper="$(command -v dex-project-context 2>/dev/null)"',
203
+ ' [[ -z "$helper" ]] && return',
204
+ ' "$helper" --root 2>/dev/null',
205
+ '}',
206
+ '_dex_linux_project_path_zsh() {',
207
+ ' local helper',
208
+ ' helper="$(command -v dex-project-context 2>/dev/null)"',
209
+ ' [[ -z "$helper" ]] && return',
210
+ ' "$helper" --path 2>/dev/null',
211
+ '}',
200
212
  '_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)"',
213
+ ' local helper context',
214
+ ' helper="$(command -v dex-project-context 2>/dev/null)"',
215
+ ' [[ -z "$helper" ]] && return',
216
+ ' context="$("$helper" 2>/dev/null)"',
205
217
  ' [[ -z "$context" ]] && return',
206
218
  ' print -n "%F{81}[$context]%f"',
207
219
  '}',
220
+ '_dex_linux_path_label_zsh() {',
221
+ ' local project_name project_path current relative',
222
+ ' project_name="$(_dex_linux_project_root_zsh)"',
223
+ ' project_path="$(_dex_linux_project_path_zsh)"',
224
+ ' current="$PWD"',
225
+ ' if [[ -n "$project_name" && -n "$project_path" ]]; then',
226
+ ' project_name="${project_name##*/}"',
227
+ ' if [[ "$current" == "$project_path" ]]; then',
228
+ ' print -r -- "$project_name"',
229
+ ' return',
230
+ ' fi',
231
+ ' relative="${current#$project_path/}"',
232
+ ' if [[ "$relative" != "$current" ]]; then',
233
+ ' print -r -- "$project_name/$relative"',
234
+ ' return',
235
+ ' fi',
236
+ ' fi',
237
+ ' print -r -- "%~"',
238
+ '}',
208
239
  '_dex_linux_prompt_zsh() {',
209
- ' local badge',
240
+ ' local badge path_label',
210
241
  ' badge="$(_dex_linux_project_badge_zsh)"',
211
- " PROMPT=$'%F{45}Dex@linux%f %F{117}%~%f\\n%F{45}>%f '",
242
+ ' path_label="$(_dex_linux_path_label_zsh)"',
243
+ " PROMPT=$'%F{45}Dex@linux%f %F{117}'\"${path_label}\"$'%f\\n%F{45}>%f '",
212
244
  ' RPROMPT="$badge"',
213
245
  '}',
214
246
  'autoload -Uz add-zsh-hook 2>/dev/null || true',
@@ -221,18 +253,50 @@ function buildLinuxShellRc({ aliases, root, label, shellName }) {
221
253
  if (shellName === 'bash') {
222
254
  return [
223
255
  ...commonLines,
256
+ '_dex_linux_project_root_bash() {',
257
+ ' local helper',
258
+ ' helper="$(command -v dex-project-context 2>/dev/null)"',
259
+ ' [[ -z "$helper" ]] && return',
260
+ ' "$helper" --root 2>/dev/null',
261
+ '}',
262
+ '_dex_linux_project_path_bash() {',
263
+ ' local helper',
264
+ ' helper="$(command -v dex-project-context 2>/dev/null)"',
265
+ ' [[ -z "$helper" ]] && return',
266
+ ' "$helper" --path 2>/dev/null',
267
+ '}',
224
268
  '_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)"',
269
+ ' local helper context',
270
+ ' helper="$(command -v dex-project-context 2>/dev/null)"',
271
+ ' [[ -z "$helper" ]] && return',
272
+ ' context="$("$helper" 2>/dev/null)"',
229
273
  ' [[ -z "$context" ]] && return',
230
274
  ' printf "\\[\\e[38;5;81m\\][%s]\\[\\e[0m\\]" "$context"',
231
275
  '}',
276
+ '_dex_linux_path_label_bash() {',
277
+ ' local project_name project_path current relative',
278
+ ' project_name="$(_dex_linux_project_root_bash)"',
279
+ ' project_path="$(_dex_linux_project_path_bash)"',
280
+ ' current="$PWD"',
281
+ ' if [[ -n "$project_name" && -n "$project_path" ]]; then',
282
+ ' project_name="${project_name##*/}"',
283
+ ' if [[ "$current" == "$project_path" ]]; then',
284
+ ' printf "%s" "$project_name"',
285
+ ' return',
286
+ ' fi',
287
+ ' relative="${current#$project_path/}"',
288
+ ' if [[ "$relative" != "$current" ]]; then',
289
+ ' printf "%s/%s" "$project_name" "$relative"',
290
+ ' return',
291
+ ' fi',
292
+ ' fi',
293
+ ' printf "%s" "\\w"',
294
+ '}',
232
295
  '_dex_linux_prompt_bash() {',
233
- ' local badge',
296
+ ' local badge path_label',
234
297
  ' 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\\] "',
298
+ ' path_label="$(_dex_linux_path_label_bash)"',
299
+ ' PS1="\\[\\e[38;5;45m\\]Dex@linux\\[\\e[0m\\] \\[\\e[38;5;117m\\]${path_label}\\[\\e[0m\\] ${badge}\\n\\[\\e[38;5;45m\\]>\\[\\e[0m\\] "',
236
300
  '}',
237
301
  'PROMPT_COMMAND=_dex_linux_prompt_bash',
238
302
  '_dex_linux_prompt_bash',
@@ -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
 
@@ -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.');
@@ -37,11 +39,8 @@ export function printHelp() {
37
39
  'dex --prompt-project-path',
38
40
  ]);
39
41
 
40
- printBlock('Scopes', [
41
- 'actual carpeta donde estas parado',
42
- 'home tu HOME del entorno actual',
43
- 'android almacenamiento accesible en modo Termux',
44
- ]);
42
+ const scopeOptions = getScopeOptions(platformMode || (isHostTermux() ? 'termux' : 'linux'));
43
+ printBlock('Scopes', scopeOptions.map((option) => `${option.key.padEnd(19, ' ')}${option.description}`));
45
44
 
46
45
  printBlock('Atajos', [
47
46
  '-h --help ayuda',
@@ -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,11 +72,21 @@ 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();
57
80
  }
58
81
 
59
82
  export function getQuickAccessRoot(platformMode) {
60
- return platformMode === 'termux' ? ANDROID_STORAGE : getHomeDirectory();
83
+ const sharedStorage = getSharedStorageRoot();
84
+
85
+ if (platformMode === 'termux') {
86
+ return sharedStorage || ANDROID_STORAGE;
87
+ }
88
+
89
+ return sharedStorage || getHomeDirectory();
61
90
  }
62
91
 
63
92
  export function getQuickAccessTitle(platformMode) {
@@ -71,13 +100,17 @@ export function getQuickAccessModeDescription(platformMode) {
71
100
  }
72
101
 
73
102
  export function getQuickAccessLabel(platformMode) {
74
- return platformMode === 'termux' ? 'ANDROID STORAGE' : 'LINUX HOME';
103
+ if (platformMode === 'termux') {
104
+ return 'ANDROID STORAGE';
105
+ }
106
+
107
+ return getSharedStorageRoot() ? 'SHARED STORAGE' : 'LINUX HOME';
75
108
  }
76
109
 
77
110
  export function getQuickAccessShortcutSummary(platformMode) {
78
111
  return platformMode === 'termux'
79
112
  ? 'dl, docs, dcim, pics, music, movies, shared'
80
- : 'home, dl, docs, desk, pics, music, vids, tmp, shared';
113
+ : 'shared, home, dl, docs, desk, pics, music, vids, tmp';
81
114
  }
82
115
 
83
116
  export function getQuickAccessAliases(platformMode) {
@@ -94,15 +127,16 @@ export function getQuickAccessAliases(platformMode) {
94
127
  }
95
128
 
96
129
  const home = getHomeDirectory();
130
+ const shared = getSharedStorageRoot() || home;
97
131
  return {
98
132
  home,
99
- shared: home,
100
- dl: path.join(home, 'Downloads'),
101
- docs: path.join(home, 'Documents'),
133
+ shared,
134
+ dl: path.join(shared, 'Download'),
135
+ docs: path.join(shared, 'Documents'),
102
136
  desk: path.join(home, 'Desktop'),
103
- pics: path.join(home, 'Pictures'),
104
- music: path.join(home, 'Music'),
105
- vids: path.join(home, 'Videos'),
137
+ pics: path.join(shared, 'Pictures'),
138
+ music: path.join(shared, 'Music'),
139
+ vids: path.join(shared, 'Movies'),
106
140
  tmp: '/tmp',
107
141
  };
108
142
  }
@@ -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);
@@ -136,7 +138,7 @@ async function detectProjectContextAt(targetDir, cwd) {
136
138
  return createContext('ruby', cwd, targetDir, await detectRubyVersion(targetDir), 'implicit');
137
139
  }
138
140
 
139
- const nestedImplicitType = await detectImplicitLanguageFromChildren(targetDir, entries);
141
+ const nestedImplicitType = await detectImplicitLanguageFromTree(targetDir);
140
142
  if (nestedImplicitType) {
141
143
  return createContext(nestedImplicitType, cwd, targetDir, await detectVersionForType(nestedImplicitType, targetDir), 'implicit');
142
144
  }
@@ -144,51 +146,60 @@ async function detectProjectContextAt(targetDir, cwd) {
144
146
  return null;
145
147
  }
146
148
 
147
- async function detectImplicitLanguageFromChildren(targetDir, entries) {
148
- const childDirectories = entries
149
- .filter((entry) => entry.isDirectory())
150
- .slice(0, 8);
149
+ async function detectImplicitLanguageFromTree(rootDir) {
150
+ const queue = [{ dir: rootDir, depth: 0 }];
151
+ let visitedEntries = 0;
151
152
 
152
- for (const entry of childDirectories) {
153
- const nestedEntries = await safeReadDir(path.join(targetDir, entry.name));
153
+ while (queue.length && visitedEntries < MAX_IMPLICIT_SCAN_ENTRIES) {
154
+ const current = queue.shift();
155
+ const nestedEntries = await safeReadDir(current.dir);
154
156
  if (!nestedEntries.length) {
155
157
  continue;
156
158
  }
157
159
 
158
- if (nestedEntries.some((item) => !item.isDirectory() && item.name.endsWith('.py'))) {
159
- return 'python';
160
- }
160
+ for (const item of nestedEntries) {
161
+ visitedEntries += 1;
161
162
 
162
- if (nestedEntries.some((item) => !item.isDirectory() && isNodeScript(item.name))) {
163
- return 'node';
164
- }
163
+ if (!item.isDirectory()) {
164
+ if (item.name.endsWith('.py')) {
165
+ return 'python';
166
+ }
165
167
 
166
- if (nestedEntries.some((item) => !item.isDirectory() && item.name.endsWith('.php'))) {
167
- return 'php';
168
- }
168
+ if (isNodeScript(item.name)) {
169
+ return 'node';
170
+ }
169
171
 
170
- if (nestedEntries.some((item) => !item.isDirectory() && item.name.endsWith('.rb'))) {
171
- return 'ruby';
172
- }
173
- }
172
+ if (item.name.endsWith('.php')) {
173
+ return 'php';
174
+ }
174
175
 
175
- return '';
176
- }
176
+ if (item.name.endsWith('.rb')) {
177
+ return 'ruby';
178
+ }
177
179
 
178
- function pickBestDetection(detections) {
179
- let bestMatch = detections[0];
180
+ continue;
181
+ }
180
182
 
181
- for (let index = 1; index < detections.length; index += 1) {
182
- const current = detections[index];
183
+ if (current.depth + 1 > MAX_IMPLICIT_SCAN_DEPTH) {
184
+ continue;
185
+ }
183
186
 
184
- if (current.type !== bestMatch.type) {
185
- break;
186
- }
187
+ if (shouldSkipImplicitDirectory(item.name)) {
188
+ continue;
189
+ }
187
190
 
188
- bestMatch = current;
191
+ queue.push({
192
+ dir: path.join(current.dir, item.name),
193
+ depth: current.depth + 1,
194
+ });
195
+ }
189
196
  }
190
197
 
191
- return bestMatch;
198
+ return '';
199
+ }
200
+
201
+ function pickBestDetection(detections) {
202
+ return detections[0];
192
203
  }
193
204
 
194
205
  function createContext(type, cwd, projectRoot, version, detectionKind) {
@@ -402,3 +413,17 @@ function firstNonEmptyLine(content) {
402
413
  function isNodeScript(name) {
403
414
  return name.endsWith('.js') || name.endsWith('.mjs') || name.endsWith('.cjs') || name.endsWith('.ts');
404
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
+ }