dex-termux-cli 0.2.3 → 0.3.0-beta.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.
@@ -0,0 +1,65 @@
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 shouldRestrictProjectToAndroidStorage(platformMode) {
60
+ return platformMode === 'termux';
61
+ }
62
+
63
+ export function isAndroidStoragePath(targetPath) {
64
+ return targetPath.startsWith('/sdcard') || targetPath.startsWith('/storage/emulated/0');
65
+ }
@@ -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 path.basename(context.projectRoot || context.cwd) || 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,78 @@ 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');
64
137
  }
65
138
 
66
139
  return null;
67
140
  }
68
141
 
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}]`;
142
+ function pickBestDetection(detections) {
143
+ let bestMatch = detections[0];
75
144
 
76
- return `Contexto: ${folderName} ${badge}`;
77
- }
145
+ for (let index = 1; index < detections.length; index += 1) {
146
+ const current = detections[index];
78
147
 
79
- export function formatPromptContext(context) {
80
- return buildContextSegment(context);
81
- }
148
+ if (current.type !== bestMatch.type) {
149
+ break;
150
+ }
82
151
 
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');
152
+ bestMatch = current;
153
+ }
154
+
155
+ return bestMatch;
90
156
  }
91
157
 
92
- function createContext(type, cwd, version) {
158
+ function createContext(type, cwd, projectRoot, version, detectionKind) {
93
159
  const meta = LANGUAGE_META[type] || { label: type.toUpperCase() };
94
160
  return {
95
161
  type,
96
162
  cwd,
163
+ projectRoot,
97
164
  label: meta.label,
98
165
  version,
166
+ detectionKind,
99
167
  };
100
168
  }
101
169
 
@@ -103,6 +171,22 @@ function buildContextSegment(context) {
103
171
  return context.label;
104
172
  }
105
173
 
174
+ function buildSearchChain(cwd) {
175
+ const chain = [];
176
+ let current = path.resolve(cwd);
177
+
178
+ for (let depth = 0; depth < MAX_PARENT_LOOKUP; depth += 1) {
179
+ chain.push(current);
180
+ const parent = path.dirname(current);
181
+ if (parent === current) {
182
+ break;
183
+ }
184
+ current = parent;
185
+ }
186
+
187
+ return chain;
188
+ }
189
+
106
190
  async function detectPythonVersion(cwd) {
107
191
  const pythonVersionFile = await readFileIfExists(path.join(cwd, '.python-version'));
108
192
  const pipfile = await readFileIfExists(path.join(cwd, 'Pipfile'));
@@ -190,7 +274,7 @@ async function detectJavaVersion(cwd) {
190
274
  return compactVersion(
191
275
  matchVersion(pom, /<java.version>([^<]+)<\/java.version>/i) ||
192
276
  matchVersion(pom, /<maven.compiler.release>([^<]+)<\/maven.compiler.release>/i) ||
193
- matchVersion(gradle, /sourceCompatibility\s*=\s*['"]?([^\s'"]+)/i) ||
277
+ matchVersion(gradle, /sourceCompatibility\s*=\s*['\"]?([^\s'\"]+)/i) ||
194
278
  matchVersion(gradleKts, /JavaLanguageVersion\.of\((\d+)\)/i),
195
279
  );
196
280
  }
@@ -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) {