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.
- package/README.md +58 -61
- package/data/commands/explain.json +63 -0
- package/package.json +5 -2
- package/src/app/main.js +11 -1
- package/src/commands/android-shell.js +49 -7
- package/src/commands/context.js +22 -0
- package/src/commands/explain.js +11 -2
- package/src/commands/install.js +279 -28
- package/src/commands/menu.js +36 -3
- package/src/commands/safe-shell.js +211 -10
- package/src/commands/search.js +5 -1
- package/src/commands/tree.js +5 -1
- package/src/core/args.js +10 -0
- package/src/core/config.js +40 -2
- package/src/core/scopes.js +22 -12
- package/src/ui/output.js +8 -6
- package/src/ui/prompt.js +76 -13
- package/src/utils/platform.js +65 -0
- package/src/utils/project-context.js +116 -32
- package/src/utils/shell.js +33 -7
|
@@ -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
|
|
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')
|
|
39
|
-
return createContext('python', cwd, await detectPythonVersion(
|
|
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')
|
|
43
|
-
return createContext('node', cwd, await detectNodeVersion(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
59
|
-
return createContext('php', cwd, await detectPhpVersion(
|
|
131
|
+
if (hasPhpFiles) {
|
|
132
|
+
return createContext('php', cwd, targetDir, await detectPhpVersion(targetDir), 'implicit');
|
|
60
133
|
}
|
|
61
134
|
|
|
62
|
-
if (
|
|
63
|
-
return createContext('ruby', cwd, await detectRubyVersion(
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
145
|
+
for (let index = 1; index < detections.length; index += 1) {
|
|
146
|
+
const current = detections[index];
|
|
78
147
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
148
|
+
if (current.type !== bestMatch.type) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
82
151
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|
package/src/utils/shell.js
CHANGED
|
@@ -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:
|
|
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 (
|
|
37
|
-
return dedupe([
|
|
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([
|
|
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) {
|