chocolatito-code 1.6.7 → 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.
Files changed (55) hide show
  1. package/README.md +437 -341
  2. package/dist/agent/context.d.ts +6 -0
  3. package/dist/agent/context.js +69 -2
  4. package/dist/agent/loop.js +8 -0
  5. package/dist/agent/toolGate.js +13 -7
  6. package/dist/agent/verifier.js +2 -1
  7. package/dist/config/permissions.d.ts +17 -0
  8. package/dist/config/permissions.js +47 -7
  9. package/dist/config/plataforma.d.ts +62 -0
  10. package/dist/config/plataforma.js +109 -0
  11. package/dist/config/updater.js +4 -1
  12. package/dist/hooks/manager.js +2 -1
  13. package/dist/index.js +99 -6
  14. package/dist/memory/manager.d.ts +15 -5
  15. package/dist/memory/manager.js +146 -29
  16. package/dist/servidor/captura.d.ts +36 -0
  17. package/dist/servidor/captura.js +74 -0
  18. package/dist/servidor/pagina.d.ts +27 -0
  19. package/dist/servidor/pagina.js +268 -0
  20. package/dist/servidor/puente.d.ts +34 -0
  21. package/dist/servidor/puente.js +115 -0
  22. package/dist/servidor/servidor.d.ts +61 -0
  23. package/dist/servidor/servidor.js +249 -0
  24. package/dist/sessions/manager.d.ts +8 -0
  25. package/dist/sessions/manager.js +44 -0
  26. package/dist/sessions/resume.d.ts +12 -0
  27. package/dist/sessions/resume.js +10 -0
  28. package/dist/tools/backgroundTask.d.ts +64 -0
  29. package/dist/tools/backgroundTask.js +264 -0
  30. package/dist/tools/browserExtension.d.ts +2 -1
  31. package/dist/tools/browserExtension.js +48 -0
  32. package/dist/tools/computerUse.js +51 -9
  33. package/dist/tools/definitions.js +93 -1
  34. package/dist/tools/gitAudit.d.ts +41 -0
  35. package/dist/tools/gitAudit.js +282 -0
  36. package/dist/tools/runCommand.js +5 -2
  37. package/dist/tools/runner.js +36 -2
  38. package/dist/tools/safety.d.ts +1 -0
  39. package/dist/tools/safety.js +3 -0
  40. package/dist/tools/todoTool.d.ts +2 -0
  41. package/dist/tools/todoTool.js +29 -0
  42. package/dist/tools/toolDefsComputer.js +9 -0
  43. package/dist/tools/win/hostScript.js +7 -2
  44. package/dist/ui/comandos.js +2 -0
  45. package/dist/ui/marco.d.ts +1 -0
  46. package/dist/ui/marco.js +4 -2
  47. package/dist/ui/permissionPrompt.d.ts +9 -0
  48. package/dist/ui/permissionPrompt.js +30 -0
  49. package/dist/ui/pieFijo.d.ts +1 -1
  50. package/dist/ui/pieFijo.js +1 -1
  51. package/dist/ui/renderer.js +8 -0
  52. package/extension/background.js +80 -0
  53. package/extension/content.js +106 -0
  54. package/extension/manifest.json +61 -60
  55. package/package.json +3 -2
@@ -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 {};
@@ -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. Look for CHOCOLATITO.md or CLAUDE.md
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 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
215
+ const osName = nombreDelSistema();
149
216
  return {
150
217
  cwd,
151
218
  os: osName,
@@ -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
  }
@@ -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
- // La firma (binario mas subcomando), NO el primer token: guardar "git" al
93
- // aprobar un "git status" autoriza por prefijo todo lo que empiece igual.
94
- const patron = finales.command
95
- ? `${toolName}:${commandSignature(String(finales.command))}`
96
- : finales.path || toolName;
97
- permissions.saveAllowPattern(String(patron));
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 };
@@ -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" : "/bin/sh";
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(["status", "tabs", "snapshot", "get_text", "screenshot", "wait_for", "notice"]),
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
- if (this.coincideExacta(p, toolName, command, detail))
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
- const bare = p.startsWith("run_command:") ? p.slice("run_command:".length) : p;
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
+ }
@@ -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
- const p = spawn("npm", args, { shell: true, windowsHide: true });
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));
@@ -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 (primeraExistente(["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/bin/sh", "/usr/bin/sh"]) ?? null);
48
+ return primeraExistente(candidatosDeBash()) ?? null;
48
49
  }
49
50
  const bases = [
50
51
  process.env["ProgramFiles"],