chore: auto-commit (43 archivos)

- .mcp.json
- bash/functions/infra/write_mcp_jupyter_config.md
- bash/functions/infra/write_mcp_jupyter_config.sh
- cpp/CMakeLists.txt
- cpp/apps/chart_demo
- cpp/apps/shaders_lab
- cpp/functions/gfx/gl_framebuffer.cpp
- cpp/functions/gfx/gl_framebuffer.h
- cpp/functions/gfx/gl_framebuffer.md
- cpp/functions/gfx/mesh_gpu.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 17:28:47 +02:00
parent a2efdcf003
commit fd5787c55f
44 changed files with 3924 additions and 64 deletions
@@ -0,0 +1,80 @@
---
name: chrome_load_extensions
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "chrome_load_extensions [--port N] [--profile DIR] --ext PATH [--ext PATH ...] [--proxy URL] [--url URL]"
description: "Lanza Chrome con extensiones unpacked via --load-extension (WSL2→Windows chrome.exe, paths traducidos, join sin echo, setsid anti-exit-144). OJO: --load-extension SOLO funciona en Chrome for Testing/Chromium/Dev. En Chrome STABLE 138+ esta DESACTIVADO (feature DisableLoadExtensionCommandLineSwitch + bloqueo duro en 148) y carga 0 extensiones aunque el cmdline sea correcto. Para Chrome stable usar install via Web Store (1-clic, persiste en perfil) o enterprise policy ExtensionInstallForcelist (requiere HKLM/HKCU Policies escribible — denegado en maquinas gestionadas)."
tags: [chrome, cdp, browser, extensions, wsl2, navegator]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
params:
- name: "--port N"
desc: "Puerto de remote debugging CDP. Default: 9222."
- name: "--profile DIR"
desc: "Chrome user-data-dir. Acepta ruta Windows (C:\\...) o ruta WSL/Linux (se traduce via wslpath -w). Default: C:\\Users\\<USERNAME>\\AppData\\Local\\fn-chrome-cdp-profile (WSL2) o /tmp/fn-chrome-cdp-profile (Linux nativo)."
- name: "--ext PATH"
desc: "Ruta a un directorio de extensión unpacked. Repetible. Acepta ruta Windows (se pasa intacta) o ruta WSL/Linux (se traduce via wslpath -w). Obligatorio al menos uno."
- name: "--proxy URL"
desc: "Proxy opcional, ej. http://127.0.0.1:8889. Agrega --proxy-server=URL a Chrome."
- name: "--url URL"
desc: "URL inicial opcional para abrir con --new-window."
output: "PID del proceso Chrome lanzado (stdout). Mensajes de estado en stderr. CDP listo en 127.0.0.1:<port>."
file_path: "bash/functions/browser/chrome_load_extensions.sh"
---
## Ejemplo
```bash
source bash/functions/browser/chrome_load_extensions.sh
chrome_load_extensions \
--port 9222 \
--profile 'C:\Users\lucas\AppData\Local\fn-chrome-cdp-profile' \
--ext 'C:\Users\lucas\hls-dl-ext' \
--ext 'C:\Users\lucas\ubol' \
--proxy http://127.0.0.1:8889 \
--url https://www.gnularetro.cc/
```
Sin proxy ni URL, sólo extensiones:
```bash
source bash/functions/browser/chrome_load_extensions.sh
pid=$(chrome_load_extensions \
--ext '/home/lucas/dev/hls-dl-ext' \
--ext '/home/lucas/dev/ubol')
# Paths WSL traducidos automáticamente a Windows.
# CDP listo en 127.0.0.1:9222.
echo "Chrome PID: $pid"
```
## Cuando usarla
Cuando necesites Chrome CDP con extensiones unpacked cargadas (HLS downloader, uBlock Origin, extensiones en desarrollo) y `chrome_launch_go_browser` no sirve porque hardcodea `--disable-extensions`. WSL2→Windows. Ideal para sesiones de navegator con proxy + extensión activa.
## Gotchas
- **MUERTO en Chrome STABLE 138+ (validado 2026-05-30, Chrome 148)**: `--load-extension` NO carga nada en el canal stable, ni con `--disable-extensions-except` ni con `--disable-features=DisableLoadExtensionCommandLineSwitch`. `chrome://version` muestra el flag correcto pero `chrome://extensions` sale vacío. Google lo bloqueó duro en stable. La función SOLO sirve en **Chrome for Testing / Chromium / Dev/Canary**, donde el switch sigue activo. Para stable: ver opciones abajo.
- **Instalar en Chrome STABLE (las que SÍ funcionan)**:
1. **Web Store 1-clic** — abre la página del store en el perfil CDP, el humano da "Añadir a Chrome". Persiste en el perfil para siempre (futuros lanzamientos ya con la extensión, sin flags). El popup de confirmación es UI del navegador (no DOM) → NO es CDP-clickable, requiere gesto humano. Único método no-admin que persiste por-perfil.
2. **Enterprise policy** `ExtensionInstallForcelist` (HKCU/HKLM `\Software\Policies\Google\Chrome`) — force-install sin clic desde el store, browser-wide. El key `Policies\Google\Chrome` puede dar "Access denied" al escribir (visto 2026-05-30 incluso en máquina personal vía reg.exe/PowerShell desde WSL — Chrome/Windows protege el subárbol Policies). Si funciona, requiere relanzar Chrome para que descargue del store. Método global (afecta todos los perfiles).
3. Extensiones **unpacked custom** (no en store, ej. un HLS downloader propio) en stable: no hay vía no-admin. Empaquetar a CRX + self-host `update_url` + policy, o usar Chrome for Testing. A menudo innecesario si la lógica vive fuera (ej. `grab_stream.py` descarga sin extensión).
- **Combo flags (solo Chrome for Testing/dev)**: requiere AMBOS `--load-extension=p1,p2` Y `--disable-extensions-except=p1,p2` juntos + `--disable-features=DisableLoadExtensionCommandLineSwitch`. **NUNCA `--disable-extensions`** (desactiva todo).
- **join sin `echo`**: rutas Windows `C:\Users\...` tienen `\U`; el `echo` de zsh (o sh con xpg_echo) lo interpreta como escape unicode y trunca la ruta a `C:`. La función usa acumulador `+=`, no `echo`. Verificable en `chrome://version` (debe verse el path completo, no `--load-extension=C:`).
- **exit 144 en Bash tool**: si el proceso Chrome retiene el pipe stdout, la herramienta devuelve exit 144. Esta función lanza con `setsid ... </dev/null >log 2>&1 &` + `disown` para desacoplar completamente. El log queda en `/tmp/chrome_ext_<port>.log`.
- **WSL2: traducir paths con `wslpath -w`**: los paths de `--ext` y `--profile` que sean rutas Linux se traducen automáticamente. Las rutas Windows (`C:\...`) se pasan intactas. `wslpath` debe estar disponible (estándar en WSL2 desde Windows 10 1903+).
- **Perfil ya abierto**: si Chrome ya tiene ese perfil abierto, relanzar añade una ventana extra a la misma instancia. La función detecta si CDP ya responde en el puerto y avisa por stderr, pero procede igualmente.
- **Web Store vs unpacked**: instalar extensiones desde la Web Store (un clic) persiste en el perfil sin necesidad de flags y sobrevive reinicios. Esta función es para extensiones unpacked en desarrollo o que no están en la Web Store. Si usas ambas, los flags no interfieren con las instaladas del store.
- **zsh globbing**: `--remote-allow-origins=*` está dentro de comillas en la función, no se expande. Si lo pasas desde la línea de comandos, entrecomillarlo.
- **Proxy + extensión**: si usas proxy para captura de tráfico (Burp, mitmproxy, gost), el proxy se aplica a toda la sesión Chrome, incluyendo el tráfico de las extensiones.
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# chrome_load_extensions — lanza Chrome (WSL2→Windows chrome.exe) con extensiones unpacked cargadas en un perfil CDP.
# Chrome 148+: requiere --load-extension=<paths> Y --disable-extensions-except=<same paths> juntos.
# NUNCA pasar --disable-extensions (desactiva todo, incluyendo las que quieres cargar).
chrome_load_extensions() {
local port=9222
local profile=""
local proxy=""
local url=""
local -a ext_paths=()
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
port="$2"; shift 2 ;;
--profile)
profile="$2"; shift 2 ;;
--ext)
ext_paths+=("$2"); shift 2 ;;
--proxy)
proxy="$2"; shift 2 ;;
--url)
url="$2"; shift 2 ;;
--*)
echo "chrome_load_extensions: flag desconocido: $1" >&2; return 1 ;;
*)
# Positional = extra ext path
ext_paths+=("$1"); shift ;;
esac
done
if [[ ${#ext_paths[@]} -eq 0 ]]; then
echo "chrome_load_extensions: se requiere al menos un --ext PATH de extension unpacked" >&2
return 1
fi
# --- Detectar chrome.exe ---
local chrome_bin=""
if command -v chrome.exe &>/dev/null; then
chrome_bin="chrome.exe"
elif [[ -f "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" ]]; then
chrome_bin="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
elif [[ -f "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" ]]; then
chrome_bin="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
else
echo "chrome_load_extensions: chrome.exe no encontrado en PATH ni en rutas conocidas" >&2
return 1
fi
# --- Detectar WSL2 ---
local wsl2=0
if grep -qi 'microsoft\|wsl' /proc/version 2>/dev/null; then
wsl2=1
fi
# --- Traducir paths de extensiones a Windows si hace falta ---
local -a win_ext_paths=()
for p in "${ext_paths[@]}"; do
if [[ $wsl2 -eq 1 ]] && [[ "$p" != [A-Za-z]:\\* ]]; then
# Path Linux → traducir a Windows
local win_p
win_p=$(wslpath -w "$p" 2>/dev/null) || {
echo "chrome_load_extensions: wslpath -w '$p' falló" >&2
return 1
}
win_ext_paths+=("$win_p")
else
win_ext_paths+=("$p")
fi
done
# --- Resolver perfil ---
if [[ -z "$profile" ]]; then
# Default: perfil canónico fn-chrome-cdp-profile en Windows
local win_user="${USERNAME:-${USER:-lucas}}"
if [[ $wsl2 -eq 1 ]]; then
profile="C:\\Users\\${win_user}\\AppData\\Local\\fn-chrome-cdp-profile"
else
profile="/tmp/fn-chrome-cdp-profile"
fi
elif [[ $wsl2 -eq 1 ]] && [[ "$profile" != [A-Za-z]:\\* ]]; then
# Path Linux del perfil → traducir a Windows
profile=$(wslpath -w "$profile" 2>/dev/null) || {
echo "chrome_load_extensions: wslpath -w '$profile' falló" >&2
return 1
}
fi
# --- Construir lista de paths separada por coma (para Chrome) ---
# Chrome usa coma como separador en --load-extension y --disable-extensions-except.
# NO usar `echo` para el join: rutas Windows como C:\Users tienen \U, y el echo de
# zsh (o sh con xpg_echo) interpreta \U como escape unicode y trunca la ruta a "C:".
# Acumulador con += y printf-safe, sin interpretacion de backslashes.
local ext_list=""
local p
for p in "${win_ext_paths[@]}"; do
ext_list+="${ext_list:+,}${p}"
done
# --- Construir args de Chrome ---
local -a args=(
"--remote-debugging-port=${port}"
"--user-data-dir=${profile}"
"--no-first-run"
"--no-default-browser-check"
"--remote-allow-origins=*"
"--load-extension=${ext_list}"
"--disable-extensions-except=${ext_list}"
# Chrome 137+ activa por defecto el feature DisableLoadExtensionCommandLineSwitch,
# que IGNORA silenciosamente --load-extension. Hay que desactivarlo o las
# extensiones unpacked no cargan (chrome://extensions sale vacio).
"--disable-features=DisableLoadExtensionCommandLineSwitch"
)
# WSL2: bind en 0.0.0.0 para que sea accesible desde la red WSL
if [[ $wsl2 -eq 1 ]]; then
args+=("--remote-debugging-address=0.0.0.0")
fi
if [[ -n "$proxy" ]]; then
args+=("--proxy-server=${proxy}")
fi
if [[ -n "$url" ]]; then
args+=("--new-window" "$url")
fi
# --- Revisar si CDP ya responde en el puerto ---
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
echo "chrome_load_extensions: CDP ya activo en puerto ${port}; lanzando ventana extra" >&2
fi
# --- Lanzar Chrome desacoplado del proceso padre ---
# setsid + redirección evita el exit 144 en el Bash tool (el pipe no queda retenido).
setsid "$chrome_bin" "${args[@]}" </dev/null >"/tmp/chrome_ext_${port}.log" 2>&1 &
local chrome_pid=$!
disown "$chrome_pid"
echo "chrome_load_extensions: Chrome lanzado PID=${chrome_pid} puerto=${port}" >&2
# --- Esperar a que CDP esté listo (hasta 15 segundos) ---
local deadline=$(( $(date +%s) + 15 ))
local ready=0
while [[ $(date +%s) -lt $deadline ]]; do
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
ready=1
break
fi
sleep 0.5
done
if [[ $ready -eq 1 ]]; then
echo "chrome_load_extensions: CDP listo en 127.0.0.1:${port}"
else
echo "chrome_load_extensions: advertencia — CDP no respondió en 15s en puerto ${port}; Chrome puede estar iniciando lentamente" >&2
fi
echo "$chrome_pid"
}