dex-termux-cli 0.1.0 → 0.2.0
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 +11 -5
- package/package.json +4 -2
- package/src/app/main.js +21 -1
- package/src/commands/context.js +26 -0
- package/src/commands/menu.js +39 -1
- package/src/commands/version.js +98 -0
- package/src/core/args.js +15 -0
- package/src/core/config.js +28 -0
- package/src/ui/output.js +82 -59
- package/src/ui/prompt.js +113 -21
- package/src/utils/project-context.js +182 -12
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
|
-
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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": [
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"safe-shell"
|
|
14
14
|
],
|
|
15
15
|
"license": "MIT",
|
|
16
|
-
"bin":
|
|
16
|
+
"bin": {
|
|
17
|
+
"dex": "bin/dex"
|
|
18
|
+
},
|
|
17
19
|
"scripts": {
|
|
18
20
|
"start": "node ./bin/dex",
|
|
19
21
|
"help": "node ./bin/dex --help"
|
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;
|
|
@@ -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
|
+
}
|
package/src/commands/menu.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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;
|
package/src/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
44
|
-
{ key: '
|
|
45
|
-
{ key: '
|
|
46
|
-
{ key: '
|
|
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
|
|
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
|
|
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('
|
|
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 =
|
|
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 ?
|
|
298
|
-
return
|
|
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[
|
|
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,
|
|
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,
|
|
43
|
+
return createContext('node', cwd, await detectNodeVersion(cwd));
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
if (names.has('Cargo.toml')) {
|
|
37
|
-
return createContext('rust', cwd,
|
|
47
|
+
return createContext('rust', cwd, await detectRustVersion(cwd));
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
if (names.has('go.mod')) {
|
|
41
|
-
return createContext('go', cwd,
|
|
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,
|
|
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,
|
|
59
|
+
return createContext('php', cwd, await detectPhpVersion(cwd));
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
if (names.has('Gemfile') || hasRubyFiles) {
|
|
53
|
-
return createContext('ruby', cwd,
|
|
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}[${
|
|
63
|
-
: `[${
|
|
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
|
|
69
|
-
return
|
|
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
|
}
|