chocolatito-code 1.6.6 → 1.6.8
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 +437 -341
- package/dist/agent/context.d.ts +6 -0
- package/dist/agent/context.js +69 -2
- package/dist/agent/loop.js +8 -0
- package/dist/agent/toolGate.js +13 -7
- package/dist/agent/verifier.js +2 -1
- package/dist/config/permissions.d.ts +17 -0
- package/dist/config/permissions.js +47 -7
- package/dist/config/plataforma.d.ts +62 -0
- package/dist/config/plataforma.js +109 -0
- package/dist/config/updater.js +4 -1
- package/dist/hooks/manager.js +2 -1
- package/dist/index.js +164 -39
- package/dist/memory/manager.d.ts +15 -5
- package/dist/memory/manager.js +146 -29
- package/dist/servidor/captura.d.ts +36 -0
- package/dist/servidor/captura.js +74 -0
- package/dist/servidor/pagina.d.ts +27 -0
- package/dist/servidor/pagina.js +268 -0
- package/dist/servidor/puente.d.ts +34 -0
- package/dist/servidor/puente.js +115 -0
- package/dist/servidor/servidor.d.ts +61 -0
- package/dist/servidor/servidor.js +249 -0
- package/dist/sessions/manager.d.ts +8 -0
- package/dist/sessions/manager.js +44 -0
- package/dist/sessions/resume.d.ts +12 -0
- package/dist/sessions/resume.js +10 -0
- package/dist/tools/backgroundTask.d.ts +64 -0
- package/dist/tools/backgroundTask.js +264 -0
- package/dist/tools/browserExtension.d.ts +2 -1
- package/dist/tools/browserExtension.js +48 -0
- package/dist/tools/computerUse.js +51 -9
- package/dist/tools/definitions.js +93 -1
- package/dist/tools/gitAudit.d.ts +41 -0
- package/dist/tools/gitAudit.js +282 -0
- package/dist/tools/runCommand.js +5 -2
- package/dist/tools/runner.js +36 -2
- package/dist/tools/safety.d.ts +1 -0
- package/dist/tools/safety.js +3 -0
- package/dist/tools/todoTool.d.ts +2 -0
- package/dist/tools/todoTool.js +29 -0
- package/dist/tools/toolDefsComputer.js +9 -0
- package/dist/tools/win/hostScript.js +7 -2
- package/dist/ui/comandos.js +2 -0
- package/dist/ui/ink/App.d.ts +0 -23
- package/dist/ui/ink/App.js +86 -57
- package/dist/ui/ink/Prompt.d.ts +16 -6
- package/dist/ui/ink/Prompt.js +37 -62
- package/dist/ui/ink/montarApp.d.ts +34 -0
- package/dist/ui/ink/montarApp.js +93 -1
- package/dist/ui/ink/prestamo.d.ts +63 -0
- package/dist/ui/ink/prestamo.js +92 -0
- package/dist/ui/ink/teclado.d.ts +48 -0
- package/dist/ui/ink/teclado.js +96 -0
- package/dist/ui/ink/transcripcion.d.ts +114 -0
- package/dist/ui/ink/transcripcion.js +180 -0
- package/dist/ui/interrupt.js +4 -1
- package/dist/ui/loopPrompt.d.ts +6 -7
- package/dist/ui/loopPrompt.js +14 -1
- package/dist/ui/marco.d.ts +3 -0
- package/dist/ui/marco.js +22 -3
- package/dist/ui/pantalla.d.ts +45 -17
- package/dist/ui/pantalla.js +150 -92
- package/dist/ui/permissionPrompt.d.ts +19 -17
- package/dist/ui/permissionPrompt.js +44 -1
- package/dist/ui/pieFijo.d.ts +1 -1
- package/dist/ui/pieFijo.js +27 -5
- package/dist/ui/renderer.js +8 -0
- package/dist/ui/selector.d.ts +6 -4
- package/dist/ui/selector.js +14 -1
- package/extension/background.js +80 -0
- package/extension/content.js +106 -0
- package/extension/manifest.json +3 -2
- package/package.json +3 -2
- package/dist/ui/historial.d.ts +0 -49
- package/dist/ui/historial.js +0 -92
package/dist/agent/context.d.ts
CHANGED
|
@@ -9,4 +9,10 @@ export interface ProjectContext {
|
|
|
9
9
|
repoMap: string;
|
|
10
10
|
}
|
|
11
11
|
export declare function olvidarMapa(cwd?: string): void;
|
|
12
|
+
interface ModularRule {
|
|
13
|
+
relPath: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function scanModularRules(baseDir: string, currentDir?: string, depth?: number): ModularRule[];
|
|
12
17
|
export declare function getProjectContext(cwd?: string): Promise<ProjectContext>;
|
|
18
|
+
export {};
|
package/dist/agent/context.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { execa } from "execa";
|
|
4
4
|
import { RULES_FILE_NAME } from "../config/constants.js";
|
|
5
5
|
import { construirMapa } from "./repoMap.js";
|
|
6
|
+
import { nombreDelSistema } from "../config/plataforma.js";
|
|
6
7
|
/** Tokens que puede ocupar el mapa dentro del prompt de sistema. */
|
|
7
8
|
const PRESUPUESTO_MAPA = 4_000;
|
|
8
9
|
/**
|
|
@@ -36,11 +37,52 @@ function mapaDe(cwd) {
|
|
|
36
37
|
cacheDeMapas.set(clave, mapa);
|
|
37
38
|
return mapa;
|
|
38
39
|
}
|
|
40
|
+
const IGNORED_RULE_DIRS = new Set(["node_modules", ".git", "dist", "tests", "assets"]);
|
|
41
|
+
const RULE_FILE_NAMES = [".chocolatitorules", ".cursorrules"];
|
|
42
|
+
export function scanModularRules(baseDir, currentDir = baseDir, depth = 0) {
|
|
43
|
+
if (depth > 3)
|
|
44
|
+
return [];
|
|
45
|
+
const rules = [];
|
|
46
|
+
try {
|
|
47
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
48
|
+
// En subdirectorios (depth >= 1), buscar archivos de reglas modulares
|
|
49
|
+
if (depth >= 1) {
|
|
50
|
+
for (const ruleFileName of RULE_FILE_NAMES) {
|
|
51
|
+
const match = entries.find((e) => e.isFile() && e.name === ruleFileName);
|
|
52
|
+
if (match) {
|
|
53
|
+
const fullPath = path.join(currentDir, match.name);
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(fullPath, "utf-8").trim();
|
|
56
|
+
if (content) {
|
|
57
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
58
|
+
rules.push({ relPath, content });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Recorrer subdirectorios recursivamente hasta profundidad 3
|
|
66
|
+
if (depth < 3) {
|
|
67
|
+
const subdirs = entries
|
|
68
|
+
.filter((e) => e.isDirectory() && !IGNORED_RULE_DIRS.has(e.name.toLowerCase()))
|
|
69
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
70
|
+
for (const subdir of subdirs) {
|
|
71
|
+
const nextDir = path.join(currentDir, subdir.name);
|
|
72
|
+
rules.push(...scanModularRules(baseDir, nextDir, depth + 1));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return rules;
|
|
78
|
+
}
|
|
39
79
|
export async function getProjectContext(cwd = process.cwd()) {
|
|
40
80
|
let rulesContent = null;
|
|
41
|
-
// 1.
|
|
81
|
+
// 1. Reglas principales de raíz: CHOCOLATITO.md, CLAUDE.md, .chocolatitorules o .cursorrules
|
|
42
82
|
const rulesPath = path.join(cwd, RULES_FILE_NAME);
|
|
43
83
|
const fallbackClaudePath = path.join(cwd, "CLAUDE.md");
|
|
84
|
+
const rootChocolatitoRules = path.join(cwd, ".chocolatitorules");
|
|
85
|
+
const rootCursorRules = path.join(cwd, ".cursorrules");
|
|
44
86
|
if (fs.existsSync(rulesPath)) {
|
|
45
87
|
try {
|
|
46
88
|
rulesContent = fs.readFileSync(rulesPath, "utf-8");
|
|
@@ -53,6 +95,31 @@ export async function getProjectContext(cwd = process.cwd()) {
|
|
|
53
95
|
}
|
|
54
96
|
catch { }
|
|
55
97
|
}
|
|
98
|
+
else if (fs.existsSync(rootChocolatitoRules)) {
|
|
99
|
+
try {
|
|
100
|
+
rulesContent = fs.readFileSync(rootChocolatitoRules, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}
|
|
104
|
+
else if (fs.existsSync(rootCursorRules)) {
|
|
105
|
+
try {
|
|
106
|
+
rulesContent = fs.readFileSync(rootCursorRules, "utf-8");
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
}
|
|
110
|
+
// Reglas modulares en subdirectorios (.chocolatitorules y .cursorrules hasta profundidad 3)
|
|
111
|
+
const modularRules = scanModularRules(cwd);
|
|
112
|
+
if (modularRules.length > 0) {
|
|
113
|
+
const modularText = modularRules
|
|
114
|
+
.map((r) => `[${r.relPath}]:\n${r.content}`)
|
|
115
|
+
.join("\n\n");
|
|
116
|
+
if (rulesContent && rulesContent.trim()) {
|
|
117
|
+
rulesContent = `${rulesContent.trim()}\n\n${modularText}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
rulesContent = modularText;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
56
123
|
// 2. Git branch & status detection
|
|
57
124
|
let gitBranch = null;
|
|
58
125
|
try {
|
|
@@ -145,7 +212,7 @@ export async function getProjectContext(cwd = process.cwd()) {
|
|
|
145
212
|
else if (fs.existsSync(cmakePath)) {
|
|
146
213
|
projectType = "C / C++ (CMake)";
|
|
147
214
|
}
|
|
148
|
-
const osName =
|
|
215
|
+
const osName = nombreDelSistema();
|
|
149
216
|
return {
|
|
150
217
|
cwd,
|
|
151
218
|
os: osName,
|
package/dist/agent/loop.js
CHANGED
|
@@ -1167,6 +1167,12 @@ export class AgentLoop {
|
|
|
1167
1167
|
const firma = commandSignature(command);
|
|
1168
1168
|
if (firma)
|
|
1169
1169
|
this.permissions.saveAllowPattern(`${fnName}:${firma}`);
|
|
1170
|
+
else
|
|
1171
|
+
this.permissions.saveAllowPattern(`${fnName}:${command}`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (fnName === "computer_use" || fnName === "chrome" || fnName === "browser" || fnName === "browser_live") {
|
|
1175
|
+
this.permissions.saveAllowPattern(fnName);
|
|
1170
1176
|
return;
|
|
1171
1177
|
}
|
|
1172
1178
|
this.permissions.saveAllowPattern(fnArgs?.path || fnName);
|
|
@@ -1191,5 +1197,7 @@ function getToolSummary(name, args) {
|
|
|
1191
1197
|
return args.dirPath;
|
|
1192
1198
|
if (args.action)
|
|
1193
1199
|
return args.action + (args.url ? ` (${args.url})` : "");
|
|
1200
|
+
if (args.taskId)
|
|
1201
|
+
return args.taskId;
|
|
1194
1202
|
return "";
|
|
1195
1203
|
}
|
package/dist/agent/toolGate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { PermissionManager, commandSignature } from "../config/permissions.js";
|
|
3
|
+
import { PermissionManager, commandSignature, isDangerousCommand } from "../config/permissions.js";
|
|
4
4
|
import { HookManager } from "../hooks/manager.js";
|
|
5
5
|
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
6
6
|
import { releaseKeyboard, retomarTeclado } from "../ui/interrupt.js";
|
|
@@ -89,12 +89,18 @@ export async function autorizarHerramienta(toolName, args, cwd = process.cwd(),
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
if (res.rememberPattern) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
if (finales.command) {
|
|
93
|
+
if (!isDangerousCommand(String(finales.command))) {
|
|
94
|
+
const firma = commandSignature(String(finales.command));
|
|
95
|
+
permissions.saveAllowPattern(`${toolName}:${firma || finales.command}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (toolName === "computer_use" || toolName === "chrome" || toolName === "browser" || toolName === "browser_live") {
|
|
99
|
+
permissions.saveAllowPattern(toolName);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
permissions.saveAllowPattern(String(finales.path || toolName));
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
return { permitida: true, args: finales };
|
package/dist/agent/verifier.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { execa } from "execa";
|
|
5
|
+
import { shellPosix } from "../config/plataforma.js";
|
|
5
6
|
/**
|
|
6
7
|
* Verificacion automatica despues de editar: se corre el linter o el
|
|
7
8
|
* comprobador de tipos del proyecto y, si falla, el error vuelve al modelo en
|
|
@@ -112,7 +113,7 @@ function elVerificadorNoSePudoEjecutar(codigo, salida) {
|
|
|
112
113
|
return /command not found|is not recognized as the name of a cmdlet|CommandNotFoundException|no such file or directory|ENOENT/i.test(salida);
|
|
113
114
|
}
|
|
114
115
|
export async function verificar(comando, cwd) {
|
|
115
|
-
const shell = process.platform === "win32" ? "powershell.exe" : "
|
|
116
|
+
const shell = process.platform === "win32" ? "powershell.exe" : shellPosix("sh");
|
|
116
117
|
const args = process.platform === "win32" ? ["-NoProfile", "-Command", comando] : ["-c", comando];
|
|
117
118
|
try {
|
|
118
119
|
const res = await execa(shell, args, {
|
|
@@ -6,6 +6,19 @@ export interface SettingsConfig {
|
|
|
6
6
|
denyPatterns?: string[];
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Acciones de SOLO OBSERVAR de cada herramienta de control.
|
|
11
|
+
*
|
|
12
|
+
* La tabla anterior estaba del reves: preguntaba por 'ui_snapshot' (que solo lee
|
|
13
|
+
* la pantalla) y dejaba pasar sin preguntar la herramienta 'chrome' entera, que
|
|
14
|
+
* conduce el navegador REAL del usuario con sus sesiones ya iniciadas. En la
|
|
15
|
+
* practica eso empujaba al usuario a poner el modo bypass para dejar de
|
|
16
|
+
* contestar dialogos inutiles, y con el bypass puesto ya no quedaba ninguna
|
|
17
|
+
* barrera justo donde hacia falta.
|
|
18
|
+
*
|
|
19
|
+
* La regla ahora es una sola: mirar es gratis, actuar se pregunta.
|
|
20
|
+
*/
|
|
21
|
+
export declare const OBSERVE_ONLY: Record<string, Set<string>>;
|
|
9
22
|
/**
|
|
10
23
|
* Una ruta esta fuera del proyecto si, resuelta, no cuelga de la raiz.
|
|
11
24
|
*
|
|
@@ -29,6 +42,10 @@ export declare function isSvgSource(script: unknown): boolean;
|
|
|
29
42
|
* Comandos seguros de desarrollo (python, npm, pytest, tsc, git status, etc.) no interrumpen al usuario.
|
|
30
43
|
*/
|
|
31
44
|
export declare function isDangerousCommand(command: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Detecta operaciones destructivas de Git que nunca deben guardarse automáticamente.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isGitDestructive(command: string): boolean;
|
|
32
49
|
/**
|
|
33
50
|
* Firma con la que se recuerda un comando aprobado.
|
|
34
51
|
*
|
|
@@ -14,7 +14,7 @@ import { isBrowserAttached } from "../tools/browserSession.js";
|
|
|
14
14
|
*
|
|
15
15
|
* La regla ahora es una sola: mirar es gratis, actuar se pregunta.
|
|
16
16
|
*/
|
|
17
|
-
const OBSERVE_ONLY = {
|
|
17
|
+
export const OBSERVE_ONLY = {
|
|
18
18
|
computer_use: new Set([
|
|
19
19
|
"screenshot",
|
|
20
20
|
"ui_snapshot",
|
|
@@ -25,7 +25,17 @@ const OBSERVE_ONLY = {
|
|
|
25
25
|
"wait",
|
|
26
26
|
"sleep",
|
|
27
27
|
]),
|
|
28
|
-
chrome: new Set([
|
|
28
|
+
chrome: new Set([
|
|
29
|
+
"status",
|
|
30
|
+
"tabs",
|
|
31
|
+
"snapshot",
|
|
32
|
+
"get_text",
|
|
33
|
+
"screenshot",
|
|
34
|
+
"wait_for",
|
|
35
|
+
"notice",
|
|
36
|
+
"console_logs",
|
|
37
|
+
"network_errors",
|
|
38
|
+
]),
|
|
29
39
|
browser: new Set(["attach", "tabs", "snapshot", "get_text", "screenshot", "wait_for", "console"]),
|
|
30
40
|
browser_live: new Set(["attach", "tabs", "snapshot", "get_text", "screenshot", "wait_for", "console"]),
|
|
31
41
|
};
|
|
@@ -208,6 +218,14 @@ export function isDangerousCommand(command) {
|
|
|
208
218
|
}
|
|
209
219
|
return false;
|
|
210
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Detecta operaciones destructivas de Git que nunca deben guardarse automáticamente.
|
|
223
|
+
*/
|
|
224
|
+
export function isGitDestructive(command) {
|
|
225
|
+
if (!command || typeof command !== "string")
|
|
226
|
+
return false;
|
|
227
|
+
return GIT_DESTRUCTIVO.some((re) => re.test(command.trim()));
|
|
228
|
+
}
|
|
211
229
|
/**
|
|
212
230
|
* Cuanto restringe cada modo. Sirve para decidir si una capa de settings sube o
|
|
213
231
|
* baja el liston: el proyecto solo puede subirlo.
|
|
@@ -389,7 +407,7 @@ export class PermissionManager {
|
|
|
389
407
|
if (MODIFYING_TOOLS.includes(toolName)) {
|
|
390
408
|
return `El modo plan es de solo lectura: "${toolName}" no puede modificar archivos ni el repositorio.`;
|
|
391
409
|
}
|
|
392
|
-
if (toolName === "run_command") {
|
|
410
|
+
if (toolName === "run_command" || toolName === "start_background_task") {
|
|
393
411
|
return "El modo plan es de solo lectura: no se ejecutan comandos. Investiga con view_file, grep_search y list_dir.";
|
|
394
412
|
}
|
|
395
413
|
// Lanzar Chrome es arrancar un proceso, no mirar: en un modo que promete
|
|
@@ -459,7 +477,7 @@ export class PermissionManager {
|
|
|
459
477
|
return !this.isPatternAllowed(toolName, args);
|
|
460
478
|
}
|
|
461
479
|
// Comandos de terminal: solo preguntar si son destructivos o peligrosos
|
|
462
|
-
if (toolName === "run_command") {
|
|
480
|
+
if (toolName === "run_command" || toolName === "start_background_task") {
|
|
463
481
|
const command = typeof args?.command === "string" ? args.command : "";
|
|
464
482
|
if (isDangerousCommand(command)) {
|
|
465
483
|
return !this.isPatternAllowed(toolName, args);
|
|
@@ -568,8 +586,15 @@ export class PermissionManager {
|
|
|
568
586
|
const p = pattern.trim();
|
|
569
587
|
if (!p)
|
|
570
588
|
continue;
|
|
571
|
-
|
|
589
|
+
// Autorizar la herramienta completa (p. ej. "computer_use", "chrome", "browser", "delete_file")
|
|
590
|
+
// o con comodin ("computer_use:*"). Esto es vital para computer_use y actuadores donde cada clic
|
|
591
|
+
// no debe volver a interrumpir tras haber seleccionado "no preguntar mas".
|
|
592
|
+
if ((p === toolName || p === `${toolName}:*`) && toolName !== "run_command" && toolName !== "start_background_task")
|
|
593
|
+
return true;
|
|
594
|
+
// Firma o coincidencia exacta de llamada, comando, firma o ruta
|
|
595
|
+
if (!encadenado && this.coincideExacta(p, toolName, command, detail))
|
|
572
596
|
return true;
|
|
597
|
+
// Coincidencia de prefijo para comandos
|
|
573
598
|
if (!encadenado && this.coincidePrefijo(p, toolName, command))
|
|
574
599
|
return true;
|
|
575
600
|
}
|
|
@@ -579,15 +604,30 @@ export class PermissionManager {
|
|
|
579
604
|
coincideExacta(p, toolName, command, detail) {
|
|
580
605
|
if (p === `${toolName}:${command || detail}`)
|
|
581
606
|
return true;
|
|
607
|
+
if ((toolName === "run_command" || toolName === "start_background_task") && command) {
|
|
608
|
+
if (p === `run_command:${command}` || p === `start_background_task:${command}`)
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
582
611
|
if (detail && (p === detail || p === `${toolName}:${detail}`))
|
|
583
612
|
return true;
|
|
613
|
+
// Normalizacion de rutas para Windows (\ vs /)
|
|
614
|
+
if (detail) {
|
|
615
|
+
const dNorm = detail.replace(/\\/g, "/");
|
|
616
|
+
const pNorm = p.replace(/\\/g, "/");
|
|
617
|
+
if (pNorm === dNorm || pNorm === `${toolName}:${dNorm}`)
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
584
620
|
return false;
|
|
585
621
|
}
|
|
586
622
|
/** Prefijo de comando ("npm", "git status"), solo al principio de la linea. */
|
|
587
623
|
coincidePrefijo(p, toolName, command) {
|
|
588
|
-
if (!command || toolName !== "run_command")
|
|
624
|
+
if (!command || (toolName !== "run_command" && toolName !== "start_background_task"))
|
|
589
625
|
return false;
|
|
590
|
-
|
|
626
|
+
let bare = p;
|
|
627
|
+
if (bare.startsWith("run_command:"))
|
|
628
|
+
bare = bare.slice("run_command:".length);
|
|
629
|
+
else if (bare.startsWith("start_background_task:"))
|
|
630
|
+
bare = bare.slice("start_background_task:".length);
|
|
591
631
|
if (!bare)
|
|
592
632
|
return false;
|
|
593
633
|
return command === bare || command.startsWith(`${bare} `);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLATAFORMAS
|
|
3
|
+
*
|
|
4
|
+
* Termux (Android) es Unix para todo menos para lo unico que importa aqui: no
|
|
5
|
+
* existe /bin. Bash y sh viven bajo el prefijo de Termux, asi que un
|
|
6
|
+
* "/bin/bash" fijo no significa "en Android funciona peor": significa que falla
|
|
7
|
+
* TODA herramienta que lance un comando, que es casi todo lo que hace el
|
|
8
|
+
* agente. Y el error que sale es un ENOENT del propio shell, que no se parece
|
|
9
|
+
* en nada a la causa.
|
|
10
|
+
*
|
|
11
|
+
* TRES COSAS QUE NO SON OBVIAS:
|
|
12
|
+
*
|
|
13
|
+
* 1. Node en Termux reporta process.platform === "android", no "linux". Por eso
|
|
14
|
+
* el campo "os" de package.json tiene que nombrarlo: si no, npm corta la
|
|
15
|
+
* instalacion con EBADPLATFORM antes de bajar un solo byte, y el usuario ni
|
|
16
|
+
* llega a ver el programa.
|
|
17
|
+
*
|
|
18
|
+
* 2. El prefijo no se puede dar por fijo. Sale de $PREFIX, que Termux exporta
|
|
19
|
+
* siempre, porque la app tambien se instala con otro id de paquete
|
|
20
|
+
* (com.termux.dev, forks de F-Droid) y ahi la ruta cambia. La constante es
|
|
21
|
+
* el ultimo recurso, no la primera opcion.
|
|
22
|
+
*
|
|
23
|
+
* 3. Hay un termux-exec que reescribe /bin/sh al vuelo y en muchos moviles esto
|
|
24
|
+
* "ya funcionaba" por el. Pero viene segun version e instalacion, asi que
|
|
25
|
+
* depender de el es que el producto funcione o no segun de donde bajo el
|
|
26
|
+
* usuario su Termux. Se resuelve la ruta y se acaba la loteria.
|
|
27
|
+
*/
|
|
28
|
+
export declare const esWindows: boolean;
|
|
29
|
+
export declare const esAndroid: boolean;
|
|
30
|
+
/** Solo por si $PREFIX no esta: la ruta de la instalacion normal de Termux. */
|
|
31
|
+
export declare const PREFIJO_TERMUX = "/data/data/com.termux/files/usr";
|
|
32
|
+
type Existe = (ruta: string) => boolean;
|
|
33
|
+
export declare function prefijoTermux(env?: NodeJS.ProcessEnv, existe?: Existe): string;
|
|
34
|
+
/**
|
|
35
|
+
* Los shells de este sistema, en el orden en que hay que probarlos.
|
|
36
|
+
*
|
|
37
|
+
* `preferencia` decide el orden, no el resultado: quien quiere bash lo quiere
|
|
38
|
+
* por comodidad (tuberias, $(), &&), y quien pide sh es porque le basta. Los dos
|
|
39
|
+
* aceptan al otro antes que quedarse sin poder ejecutar nada.
|
|
40
|
+
*/
|
|
41
|
+
export declare function candidatosDeShell(preferencia?: "bash" | "sh", prefijo?: string): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Separado de shellPosix() para poder probar Android sin un Android: recibe la
|
|
44
|
+
* plataforma, el prefijo y quien sabe si un archivo existe.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolverShell(opciones: {
|
|
47
|
+
plataforma: string;
|
|
48
|
+
preferencia?: "bash" | "sh";
|
|
49
|
+
prefijo?: string;
|
|
50
|
+
existe: Existe;
|
|
51
|
+
}): string;
|
|
52
|
+
/** El shell con el que ejecutar una linea de comandos en un sistema Unix. */
|
|
53
|
+
export declare function shellPosix(preferencia?: "bash" | "sh"): string;
|
|
54
|
+
/** Candidatos de bash de este sistema, en orden. Para los hooks. */
|
|
55
|
+
export declare function candidatosDeBash(): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Como se le nombra el sistema al modelo. Android se dice con Termux delante
|
|
58
|
+
* porque de "Linux" el modelo deduce sudo y apt-get, y ahi no hay ninguno de los
|
|
59
|
+
* dos: se instala con pkg y no hay root.
|
|
60
|
+
*/
|
|
61
|
+
export declare function nombreDelSistema(plataforma?: string): string;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* PLATAFORMAS
|
|
5
|
+
*
|
|
6
|
+
* Termux (Android) es Unix para todo menos para lo unico que importa aqui: no
|
|
7
|
+
* existe /bin. Bash y sh viven bajo el prefijo de Termux, asi que un
|
|
8
|
+
* "/bin/bash" fijo no significa "en Android funciona peor": significa que falla
|
|
9
|
+
* TODA herramienta que lance un comando, que es casi todo lo que hace el
|
|
10
|
+
* agente. Y el error que sale es un ENOENT del propio shell, que no se parece
|
|
11
|
+
* en nada a la causa.
|
|
12
|
+
*
|
|
13
|
+
* TRES COSAS QUE NO SON OBVIAS:
|
|
14
|
+
*
|
|
15
|
+
* 1. Node en Termux reporta process.platform === "android", no "linux". Por eso
|
|
16
|
+
* el campo "os" de package.json tiene que nombrarlo: si no, npm corta la
|
|
17
|
+
* instalacion con EBADPLATFORM antes de bajar un solo byte, y el usuario ni
|
|
18
|
+
* llega a ver el programa.
|
|
19
|
+
*
|
|
20
|
+
* 2. El prefijo no se puede dar por fijo. Sale de $PREFIX, que Termux exporta
|
|
21
|
+
* siempre, porque la app tambien se instala con otro id de paquete
|
|
22
|
+
* (com.termux.dev, forks de F-Droid) y ahi la ruta cambia. La constante es
|
|
23
|
+
* el ultimo recurso, no la primera opcion.
|
|
24
|
+
*
|
|
25
|
+
* 3. Hay un termux-exec que reescribe /bin/sh al vuelo y en muchos moviles esto
|
|
26
|
+
* "ya funcionaba" por el. Pero viene segun version e instalacion, asi que
|
|
27
|
+
* depender de el es que el producto funcione o no segun de donde bajo el
|
|
28
|
+
* usuario su Termux. Se resuelve la ruta y se acaba la loteria.
|
|
29
|
+
*/
|
|
30
|
+
export const esWindows = process.platform === "win32";
|
|
31
|
+
export const esAndroid = process.platform === "android";
|
|
32
|
+
/** Solo por si $PREFIX no esta: la ruta de la instalacion normal de Termux. */
|
|
33
|
+
export const PREFIJO_TERMUX = "/data/data/com.termux/files/usr";
|
|
34
|
+
const existeEnDisco = (ruta) => {
|
|
35
|
+
try {
|
|
36
|
+
return fs.existsSync(ruta);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export function prefijoTermux(env = process.env, existe = existeEnDisco) {
|
|
43
|
+
const p = env["PREFIX"];
|
|
44
|
+
if (p && existe(path.posix.join(p, "bin")))
|
|
45
|
+
return p;
|
|
46
|
+
return PREFIJO_TERMUX;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Los shells de este sistema, en el orden en que hay que probarlos.
|
|
50
|
+
*
|
|
51
|
+
* `preferencia` decide el orden, no el resultado: quien quiere bash lo quiere
|
|
52
|
+
* por comodidad (tuberias, $(), &&), y quien pide sh es porque le basta. Los dos
|
|
53
|
+
* aceptan al otro antes que quedarse sin poder ejecutar nada.
|
|
54
|
+
*/
|
|
55
|
+
export function candidatosDeShell(preferencia = "bash", prefijo) {
|
|
56
|
+
const bash = [
|
|
57
|
+
prefijo ? path.posix.join(prefijo, "bin", "bash") : undefined,
|
|
58
|
+
"/bin/bash",
|
|
59
|
+
"/usr/bin/bash",
|
|
60
|
+
"/usr/local/bin/bash",
|
|
61
|
+
];
|
|
62
|
+
const sh = [prefijo ? path.posix.join(prefijo, "bin", "sh") : undefined, "/bin/sh", "/usr/bin/sh"];
|
|
63
|
+
const orden = preferencia === "bash" ? [...bash, ...sh] : [...sh, ...bash];
|
|
64
|
+
return orden.filter((r) => !!r);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Separado de shellPosix() para poder probar Android sin un Android: recibe la
|
|
68
|
+
* plataforma, el prefijo y quien sabe si un archivo existe.
|
|
69
|
+
*/
|
|
70
|
+
export function resolverShell(opciones) {
|
|
71
|
+
const { plataforma, preferencia = "bash", existe } = opciones;
|
|
72
|
+
const enAndroid = plataforma === "android";
|
|
73
|
+
const prefijo = enAndroid ? opciones.prefijo || PREFIJO_TERMUX : undefined;
|
|
74
|
+
for (const candidato of candidatosDeShell(preferencia, prefijo)) {
|
|
75
|
+
if (existe(candidato))
|
|
76
|
+
return candidato;
|
|
77
|
+
}
|
|
78
|
+
// Ni uno. En Android se devuelve la ruta de Termux y no /bin/sh: asi el ENOENT
|
|
79
|
+
// nombra una ruta que el usuario reconoce y "pkg install bash" se ve venir.
|
|
80
|
+
const nombre = preferencia === "bash" ? "bash" : "sh";
|
|
81
|
+
return prefijo ? path.posix.join(prefijo, "bin", nombre) : `/bin/${nombre}`;
|
|
82
|
+
}
|
|
83
|
+
/** El shell con el que ejecutar una linea de comandos en un sistema Unix. */
|
|
84
|
+
export function shellPosix(preferencia = "bash") {
|
|
85
|
+
return resolverShell({
|
|
86
|
+
plataforma: process.platform,
|
|
87
|
+
preferencia,
|
|
88
|
+
prefijo: esAndroid ? prefijoTermux() : undefined,
|
|
89
|
+
existe: existeEnDisco,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Candidatos de bash de este sistema, en orden. Para los hooks. */
|
|
93
|
+
export function candidatosDeBash() {
|
|
94
|
+
return candidatosDeShell("bash", esAndroid ? prefijoTermux() : undefined);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Como se le nombra el sistema al modelo. Android se dice con Termux delante
|
|
98
|
+
* porque de "Linux" el modelo deduce sudo y apt-get, y ahi no hay ninguno de los
|
|
99
|
+
* dos: se instala con pkg y no hay root.
|
|
100
|
+
*/
|
|
101
|
+
export function nombreDelSistema(plataforma = process.platform) {
|
|
102
|
+
if (plataforma === "win32")
|
|
103
|
+
return "Windows";
|
|
104
|
+
if (plataforma === "darwin")
|
|
105
|
+
return "macOS";
|
|
106
|
+
if (plataforma === "android")
|
|
107
|
+
return "Android (Termux)";
|
|
108
|
+
return "Linux";
|
|
109
|
+
}
|
package/dist/config/updater.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { esAndroid, shellPosix } from "./plataforma.js";
|
|
5
6
|
/**
|
|
6
7
|
* Actualizaciones solas, sin que el usuario tenga que enterarse.
|
|
7
8
|
*
|
|
@@ -161,7 +162,9 @@ export function instalar(version, ejecutar = spawnNpm) {
|
|
|
161
162
|
function spawnNpm(args) {
|
|
162
163
|
return new Promise((resolve) => {
|
|
163
164
|
// shell:true porque en Windows `npm` es un .cmd y sin shell no se encuentra.
|
|
164
|
-
|
|
165
|
+
// En Android hay que decir cual: shell:true significa "/bin/sh", que en
|
|
166
|
+
// Termux no existe, y la actualizacion fallaria sin explicar por que.
|
|
167
|
+
const p = spawn("npm", args, { shell: esAndroid ? shellPosix("sh") : true, windowsHide: true });
|
|
165
168
|
let salida = "";
|
|
166
169
|
p.stdout?.on("data", (d) => (salida += d));
|
|
167
170
|
p.stderr?.on("data", (d) => (salida += d));
|
package/dist/hooks/manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
+
import { candidatosDeBash } from "../config/plataforma.js";
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
8
|
/**
|
|
8
9
|
* EL SHELL DE LOS HOOKS, RESUELTO A UNA RUTA QUE EXISTE
|
|
@@ -44,7 +45,7 @@ function buscarBash() {
|
|
|
44
45
|
// /bin/sh va al final a proposito: en Alpine, NixOS o una imagen slim es el
|
|
45
46
|
// unico que hay, y un hook de una linea funciona igual. Preferir un shell
|
|
46
47
|
// que si existe es mejor que no ejecutar la politica del usuario.
|
|
47
|
-
return
|
|
48
|
+
return primeraExistente(candidatosDeBash()) ?? null;
|
|
48
49
|
}
|
|
49
50
|
const bases = [
|
|
50
51
|
process.env["ProgramFiles"],
|