3 Commits

Author SHA1 Message Date
egutierrez 648ce63fc0 chore: auto-commit (1 archivos)
- .claude/settings.local.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 01:38:36 +02:00
Egutierrez 685224ccb2 fix(browser): guards chromium-por-udd dejan de auto-matchear el propio grep
Bug descubierto al ejecutar el reset real: los guards y los kills usaban
'pgrep -af [c]hromium | grep -F <udd>'. Como la ruta del user-data-dir contiene
la cadena 'chromium' (~/.config/chromium-cdp), el propio proceso grep/ugrep —cuyo
cmdline incluye <udd>— era capturado por pgrep, dando un falso positivo perpetuo:
el guard creía siempre que había un chromium abierto y delete/restore abortaban
con exit 2, y el lazo de cierre nunca convergía.

Fix en delete_chrome_profile, restore_chrome_bookmarks, create_chrome_profile y el
pipeline reset_chrome_profiles: enumerar por PID con 'pgrep -x chromium' (comm
exactamente 'chromium', nunca grep/pgrep/bash) y leer /proc/PID/cmdline para
comprobar el udd. Validado: reset destructivo real de los 4 perfiles completó OK,
cada perfil quedó con solo uBlock + web_proxy y los bookmarks restaurados.
2026-06-06 01:37:51 +02:00
Egutierrez ae841ceedb feat(browser): CRUD de perfiles Chromium + pipeline reset_chrome_profiles
Cinco funciones nuevas (dominio browser, grupo navegator) que cierran los gaps
de gestión de perfiles, más un pipeline que las orquesta:

- backup_chrome_bookmarks / restore_chrome_bookmarks: backup y restore de los
  archivos Bookmarks (copia byte a byte verbatim para preservar el checksum
  interno; en Chromium 148 los bookmarks no están bajo el super_mac de Secure
  Preferences). Guard por user-data-dir (no global).
- delete_chrome_profile: borra la carpeta del perfil + limpia su entrada en
  Local State (info_cache, profiles_order, last_active_profiles, last_used).
- create_chrome_profile: lanza chromium headless (vía systemd-run) para que la
  managed policy instale la whitelist de extensiones, y asigna el nombre legible
  en Local State. Mata todo el árbol de chromium del udd antes de editar Local
  State (los hijos zygote/gpu no repiten --user-data-dir pero referencian la ruta).
- list_chrome_profile_extensions (Go): lista extensiones de un perfil con
  ID/name/version/location/enabled/fromPolicy. 7 unit tests.
- reset_chrome_profiles (pipeline): backup -> cerrar chromium -> delete -> create
  -> restore -> verify. Destructivo (--yes), --dry-run seguro.

Validado: unit tests Go verdes, backup/restore byte-idéntico, delete limpia Local
State, create instala la forcelist global (uBlock + web_proxy) en perfiles nuevos.
2026-06-06 01:24:21 +02:00
14 changed files with 2028 additions and 5 deletions
+11 -5
View File
@@ -1,4 +1,14 @@
{
"permissions": {
"allow": [
"Bash(CGO_ENABLED=1 go test *)",
"Bash(sqlite3 *)"
]
},
"enabledMcpjsonServers": [
"registry",
"jupyter"
],
"hooks": {
"PreToolUse": [
{
@@ -49,9 +59,5 @@
]
}
]
},
"enabledMcpjsonServers": [
"registry",
"jupyter"
]
}
}
@@ -0,0 +1,79 @@
---
name: backup_chrome_bookmarks
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "backup_chrome_bookmarks --user-data-dir <dir> [--profile <name>]... [--backup-dir <dir>] [--dry-run]"
description: "Copia byte a byte los archivos Bookmarks de perfiles Chrome/Chromium a un directorio de backup con timestamp ISO. Descubre automáticamente todos los perfiles con Bookmarks si no se especifica ninguno. Preserva el checksum interno del archivo sin parsear ni reserializar el JSON. No requiere que Chromium esté cerrado."
tags: [navegator, chromium, bookmarks, backup, browser, scraping]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/backup_chrome_bookmarks.sh"
params:
- name: --user-data-dir
desc: "(obligatorio) Ruta raíz del user-data-dir de Chrome/Chromium. Ej: ~/.config/chromium-cdp"
- name: --profile
desc: "Nombre de carpeta de perfil a respaldar (repetible). Si no se pasa ninguno se descubren todos los perfiles que contengan un archivo Bookmarks, excluyendo System Profile."
- name: --backup-dir
desc: "Directorio raíz donde se crearán los backups. Default: ~/.local/share/web_scraping/bookmarks-backups"
- name: --dry-run
desc: "Muestra a stderr qué archivos se copiarían y sus tamaños sin escribir nada en disco. El JSON de salida se emite igualmente."
output: "JSON en stdout: {backup_dir: \"<dir>\", ts: \"<YYYYMMDDTHHmmss>\", profiles: [{profile: \"<name>\", src: \"<path>\", dst: \"<path>\", bytes: N}, ...]}. Perfiles sin Bookmarks se omiten silenciosamente. Exit 0 en éxito o dry-run. Errores a stderr con exit != 0."
---
## Ejemplo
```bash
# Backup de todos los perfiles del chromium-cdp (descubrimiento automático)
source $HOME/fn_registry/bash/functions/browser/backup_chrome_bookmarks.sh
backup_chrome_bookmarks --user-data-dir "$HOME/.config/chromium-cdp"
# Previsualizar sin tocar nada
backup_chrome_bookmarks \
--user-data-dir "$HOME/.config/chromium-cdp" \
--dry-run
# Backup de perfiles específicos
backup_chrome_bookmarks \
--user-data-dir "$HOME/.config/chromium-cdp" \
--profile Default \
--profile Personal \
--profile "Profile 1"
# Backup a directorio personalizado
backup_chrome_bookmarks \
--user-data-dir "$HOME/.config/chromium-cdp" \
--backup-dir "$HOME/vaults/backups/bookmarks"
# Salida esperada (ejemplo):
# {"backup_dir":"/home/enmanuel/.local/share/web_scraping/bookmarks-backups","ts":"20260605T143022","profiles":[{"profile":"Default","src":"/home/enmanuel/.config/chromium-cdp/Default/Bookmarks","dst":"/home/enmanuel/.local/share/web_scraping/bookmarks-backups/20260605T143022/Default/Bookmarks","bytes":4218}]}
```
También ejecutable directamente con `fn run`:
```bash
cd $HOME/fn_registry
./fn run backup_chrome_bookmarks_bash_browser -- \
--user-data-dir "$HOME/.config/chromium-cdp" --dry-run
```
## Cuando usarla
Úsala antes de cualquier sesión de scraping o automatización que modifique bookmarks de Chromium, para tener un snapshot recuperable. También útil como paso previo en pipelines que reorganizan o importan marcadores masivamente. Combínala con `rotate_backups_bash_infra` para aplicar política de retención sobre el directorio de backups.
## Gotchas
- **Copia verbatim para preservar checksum**: el archivo `Bookmarks` de Chromium incluye un campo `checksum` calculado sobre el contenido. Esta función usa `cp -p` sin tocar el contenido — si parseases y reserializases el JSON (con `jq`, `python3 json.dump`, etc.) el checksum quedaría inválido y Chromium podría resetear o ignorar los marcadores al arrancar.
- **No requiere Chromium cerrado**: a diferencia de `clean_chrome_profile_extensions`, esta función solo lee el archivo `Bookmarks`. Chromium no mantiene un lock exclusivo sobre él — la copia es segura con el navegador abierto. El archivo refleja el estado en disco en ese instante; cambios en vuelo en memoria no estarán en el backup hasta que Chromium los persista.
- **Perfiles sin Bookmarks se omiten silenciosamente**: si un perfil existe pero no tiene el archivo `Bookmarks` (perfil recién creado sin haber abierto el navegador), se salta sin error. Solo aparece en el JSON de salida si fue respaldado.
- **System Profile excluido siempre**: el perfil `System Profile` es un perfil interno de Chromium sin datos de usuario y se excluye del descubrimiento automático.
- **Sin jq ni python3**: la emisión del JSON de salida se construye con printf de bash puro, sin dependencias externas.
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# backup_chrome_bookmarks — copia byte a byte los archivos Bookmarks de perfiles
# Chrome/Chromium a un directorio de backup con timestamp. Preserva el checksum
# interno del archivo sin parsear ni reserializar el JSON.
set -euo pipefail
backup_chrome_bookmarks() {
# ── defaults ──────────────────────────────────────────────────────────────
local _user_data_dir=""
local _profiles=()
local _backup_dir="${HOME}/.local/share/web_scraping/bookmarks-backups"
local _dry_run=0
# ── parse args ─────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: backup_chrome_bookmarks --user-data-dir <dir> [--profile <name>]...
[--backup-dir <dir>] [--dry-run]
--user-data-dir (obligatorio) Raíz de perfiles de Chrome/Chromium.
Ej: ~/.config/chromium-cdp
--profile <name> Nombre de carpeta de perfil a respaldar (repetible).
Si no se pasa ninguno → respalda TODOS los perfiles con
un archivo Bookmarks (excluye System Profile).
--backup-dir <dir> Directorio raíz para backups.
Default: ~/.local/share/web_scraping/bookmarks-backups
--dry-run Muestra qué copiaría sin tocar nada.
Exit codes:
0 éxito (o dry-run completado)
1 error de argumento o validación
EOF
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _user_data_dir="$2"; shift 2 ;;
--profile) _profiles+=("$2"); shift 2 ;;
--backup-dir) _backup_dir="$2"; shift 2 ;;
--dry-run) _dry_run=1; shift ;;
-h|--help) _usage; return 0 ;;
*) echo "backup_chrome_bookmarks: argumento desconocido: $1" >&2; return 1 ;;
esac
done
# ── validaciones ──────────────────────────────────────────────────────────
if [[ -z "$_user_data_dir" ]]; then
echo "backup_chrome_bookmarks: --user-data-dir es obligatorio" >&2
return 1
fi
if [[ ! -d "$_user_data_dir" ]]; then
echo "backup_chrome_bookmarks: directorio no encontrado: ${_user_data_dir}" >&2
return 1
fi
# ── descubrir perfiles si no se pasó ninguno ───────────────────────────────
if [[ ${#_profiles[@]} -eq 0 ]]; then
local _candidate
while IFS= read -r -d '' _candidate; do
local _pname
_pname="$(basename "$_candidate")"
# Excluir System Profile (perfil interno de Chromium sin datos de usuario)
if [[ "$_pname" == "System Profile" ]]; then
continue
fi
if [[ -f "${_candidate}/Bookmarks" ]]; then
_profiles+=("$_pname")
fi
done < <(find "$_user_data_dir" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
fi
if [[ ${#_profiles[@]} -eq 0 ]]; then
echo "backup_chrome_bookmarks: no se encontraron perfiles con archivo Bookmarks en: ${_user_data_dir}" >&2
return 1
fi
# ── timestamp único para este backup ──────────────────────────────────────
local _ts
_ts="$(date +%Y%m%dT%H%M%S)"
# ── procesar perfiles ─────────────────────────────────────────────────────
# Construir el array de resultados JSON manualmente (sin jq ni python3)
local _results="["
local _first=1
local _profile
for _profile in "${_profiles[@]}"; do
local _src="${_user_data_dir}/${_profile}/Bookmarks"
# Si el perfil no tiene Bookmarks, se omite sin error
if [[ ! -f "$_src" ]]; then
continue
fi
local _dst="${_backup_dir}/${_ts}/${_profile}/Bookmarks"
local _bytes
_bytes="$(wc -c < "$_src")"
if [[ $_dry_run -eq 1 ]]; then
echo "backup_chrome_bookmarks: [dry-run] cp -p \"${_src}\" -> \"${_dst}\" (${_bytes} bytes)" >&2
else
local _dst_dir
_dst_dir="$(dirname "$_dst")"
mkdir -p "$_dst_dir"
cp -p "$_src" "$_dst"
fi
# Escapar comillas dobles en el path por si acaso
local _src_esc="${_src//\"/\\\"}"
local _dst_esc="${_dst//\"/\\\"}"
local _profile_esc="${_profile//\"/\\\"}"
local _entry
_entry="$(printf '{"profile":"%s","src":"%s","dst":"%s","bytes":%s}' \
"$_profile_esc" "$_src_esc" "$_dst_esc" "$_bytes")"
if [[ $_first -eq 1 ]]; then
_results+="$_entry"
_first=0
else
_results+=",${_entry}"
fi
done
_results+="]"
# ── emitir resultado JSON ──────────────────────────────────────────────────
local _backup_dir_esc="${_backup_dir//\"/\\\"}"
printf '{"backup_dir":"%s","ts":"%s","profiles":%s}\n' \
"$_backup_dir_esc" "$_ts" "$_results"
}
# ── auto-ejecución ────────────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
backup_chrome_bookmarks "$@"
fi
@@ -0,0 +1,93 @@
---
name: create_chrome_profile
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "create_chrome_profile --user-data-dir <dir> --profile <dir-name> --name <legible> [--port N] [--chrome-path <path>] [--no-launch] [--timeout-sec N] [--dry-run]"
description: "Crea un perfil Chrome/Chromium nuevo en un user-data-dir: opcionalmente lanza chromium headless vía systemd-run para que la managed policy instale las extensiones forzadas (uBlock, web_proxy) y luego edita Local State para asignar el nombre legible al perfil. Con --no-launch crea solo la estructura de carpetas y la entrada en Local State sin arrancar Chrome."
tags: [navegator, chromium, profile, browser, cdp, headless, scraping]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/create_chrome_profile.sh"
params:
- name: --user-data-dir
desc: "Raíz del user-data-dir de Chrome/Chromium. Puede no existir; la función lo crea. Obligatorio."
- name: --profile
desc: "Nombre de la carpeta del perfil dentro de user-data-dir, por ejemplo: Default, \"Profile 1\", Automation. Obligatorio."
- name: --name
desc: "Nombre legible visible en el selector de perfil de Chrome, por ejemplo: Work, Aurgi, Bot. Obligatorio."
- name: --port
desc: "Puerto CDP para el lanzamiento headless. Default: 9250. Usar un valor distinto al 9222 global para no colisionar."
- name: --chrome-path
desc: "Ruta absoluta al binario chromium/chrome. Si se omite, auto-detecta: chromium, chromium-browser, google-chrome, brave-browser."
- name: --no-launch
desc: "No lanza chromium. Solo crea la carpeta del perfil y edita Local State con el nombre legible. El perfil no tendrá extensiones instaladas. Útil para tests y CRUD offline."
- name: --timeout-sec
desc: "Segundos máximos esperando a que Preferences aparezca tras el lanzamiento headless. Default: 25."
- name: --dry-run
desc: "Describe las acciones que se ejecutarían sin lanzar ni escribir nada. Emite el JSON de resultado con dry_run:true."
output: "JSON en stdout: {\"profile\":\"<dir-name>\",\"name\":\"<legible>\",\"launched\":true|false,\"preferences_created\":true|false}. En dry-run añade \"dry_run\":true. Exit 0 en éxito."
---
## Ejemplo
```bash
source $HOME/fn_registry/bash/functions/browser/create_chrome_profile.sh
# Modo offline (no lanza Chrome, solo CRUD de Local State — seguro para tests)
create_chrome_profile \
--user-data-dir /tmp/test_udd \
--profile "Automation" \
--name "Aurgi Bot" \
--no-launch
# Salida: {"profile":"Automation","name":"Aurgi Bot","launched":false,"preferences_created":false}
# Modo normal: lanza headless para que la policy instale uBlock y web_proxy,
# luego asigna nombre en Local State
create_chrome_profile \
--user-data-dir "$HOME/.local/share/web_scraping/profiles" \
--profile "Profile 1" \
--name "Work" \
--port 9250
# Salida: {"profile":"Profile 1","name":"Work","launched":true,"preferences_created":true}
# Dry-run: describe acciones sin ejecutar nada
create_chrome_profile \
--user-data-dir "$HOME/.local/share/web_scraping/profiles" \
--profile "Default" \
--name "Scraping" \
--dry-run
```
## Cuando usarla
Úsala para aprovisionar perfiles nuevos en un user-data-dir de automatización antes de lanzar sesiones CDP con `script-navegador` o funciones del grupo `navegator`. En modo normal (sin `--no-launch`) la managed policy instala automáticamente uBlock y la extensión web_proxy en el perfil nuevo; en `--no-launch` sirve para tests unitarios o para crear la entrada de Local State sin depender de Chrome.
## Gotchas
- **Lanzar chromium desde Bash tool de Claude da exit-144**: la función usa `systemd-run --user --collect` para aislar el proceso en su propio cgroup, evitando que el harness del agente lo mate. Esto es obligatorio; lanzar con `&` / `setsid` daría exit-144 en el contexto del agente.
- **La managed policy instala las extensiones al arrancar el perfil**: NO pasar `--disable-extensions` — rompería la forcelist. Las extensiones force-listed (`ExtensionInstallForcelist` en `/etc/chromium/policies/managed/extensions.json`) se instalan en el perfil durante el primer arranque; en el headless inicial puede no completar la descarga si no hay red o si el timeout es corto.
- **Dos chromium NO pueden compartir el mismo user-data-dir**: si ya hay un chromium corriendo sobre `--user-data-dir`, la función detecta `SingletonLock` y sale con exit 2 antes de lanzar. Para perfiles de automatización paralela, usa un `--user-data-dir` dedicado por perfil.
- **Local State debe editarse con Chrome muerto**: la función para el unit de systemd y espera la desaparición de `SingletonLock` antes de editar `Local State`. Si se edita mientras Chrome está vivo, Chrome sobreescribe el archivo desde memoria al salir y los cambios de nombre se pierden.
- **`--remote-allow-origins=*` necesita comillas en zsh**: el glob `*` se expande si no va entre comillas. La función pasa el flag correctamente internamente, pero si lo pasas tú en otros scripts acuérdate de las comillas.
- **Perfil diario en `~/.config/chromium-cdp`**: en este equipo el fragmento `/etc/chromium.d/cdp` redirige el user-data-dir global a `~/.config/chromium-cdp`. Para automatización usar siempre un `--user-data-dir` dedicado fuera de `~/.config/`.
- **Timeout corto puede dar `preferences_created: false`**: el perfil headless tarda entre 2-8 segundos en crear `Preferences` según la carga del sistema. Si se aumenta `--timeout-sec` a 45-60 en máquinas lentas se evitan falsos timeouts.
## Exit codes
| Código | Significado |
|--------|------------|
| 0 | Éxito |
| 1 | Argumento obligatorio faltante o binario no encontrado |
| 2 | Lock: ya hay un chromium usando el mismo user-data-dir |
| 3 | Timeout esperando a que Preferences se cree |
| 4 | Error editando Local State (JSON inválido tras escritura) |
@@ -0,0 +1,309 @@
#!/usr/bin/env bash
# create_chrome_profile — crea un perfil Chrome/Chromium nuevo en un user-data-dir,
# opcionalmente lanzando chromium headless para que la managed policy instale las
# extensiones forzadas (uBlock, web_proxy). Edita Local State para asignar el nombre
# legible al perfil.
set -euo pipefail
create_chrome_profile() {
# ── defaults ──────────────────────────────────────────────────────────────
local _udd=""
local _profile_dir=""
local _name=""
local _port=9250
local _chrome_path=""
local _no_launch=0
local _timeout_sec=25
local _dry_run=0
# ── parse args ─────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: create_chrome_profile --user-data-dir <dir> --profile <dir-name> --name <legible>
[--port N] [--chrome-path <path>] [--no-launch] [--timeout-sec N] [--dry-run]
--user-data-dir Raíz del user-data-dir de Chrome/Chromium (obligatorio).
--profile Nombre de la carpeta del perfil dentro de user-data-dir, ej: Default,
"Profile 1", Automation (obligatorio).
--name Nombre legible que aparece en el selector de perfil, ej: Work, Aurgi
(obligatorio).
--port Puerto CDP para el lanzamiento headless. Default: 9250.
Usar un puerto distinto al 9222 global para no chocar.
--chrome-path Ruta explícita al binario chromium/chrome. Auto-detecta si se omite.
--no-launch No lanza chromium. Crea la carpeta y edita Local State offline.
El perfil no tendrá extensiones instaladas; útil para tests/CRUD.
--timeout-sec Segundos esperando a que Preferences aparezca tras el lanzamiento.
Default: 25.
--dry-run Describe las acciones sin lanzar ni escribir nada.
Exit codes:
0 éxito
1 error de argumento o validación
2 lock: ya hay un chromium usando este user-data-dir
3 timeout esperando a que Preferences se cree
4 error editando Local State (JSON inválido tras escritura)
EOF
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _udd="$2"; shift 2 ;;
--profile) _profile_dir="$2"; shift 2 ;;
--name) _name="$2"; shift 2 ;;
--port) _port="$2"; shift 2 ;;
--chrome-path) _chrome_path="$2"; shift 2 ;;
--no-launch) _no_launch=1; shift ;;
--timeout-sec) _timeout_sec="$2"; shift 2 ;;
--dry-run) _dry_run=1; shift ;;
-h|--help) _usage; return 0 ;;
*) echo "create_chrome_profile: argumento desconocido: $1" >&2; return 1 ;;
esac
done
# ── validaciones obligatorias ──────────────────────────────────────────────
if [[ -z "$_udd" ]]; then
echo "create_chrome_profile: --user-data-dir es obligatorio" >&2
return 1
fi
if [[ -z "$_profile_dir" ]]; then
echo "create_chrome_profile: --profile es obligatorio" >&2
return 1
fi
if [[ -z "$_name" ]]; then
echo "create_chrome_profile: --name es obligatorio" >&2
return 1
fi
local _profile_path="${_udd}/${_profile_dir}"
local _local_state="${_udd}/Local State"
local _prefs_file="${_profile_path}/Preferences"
# ── guard: lock por user-data-dir ─────────────────────────────────────────
# Dos procesos chromium no pueden compartir el mismo user-data-dir.
if [[ $_dry_run -eq 0 && $_no_launch -eq 0 ]]; then
local _singleton="${_udd}/SingletonLock"
if [[ -e "$_singleton" ]]; then
echo "create_chrome_profile: ya hay un chromium corriendo con --user-data-dir=${_udd}" >&2
echo " (encontrado: ${_singleton})" >&2
echo " Ciérralo o usa un user-data-dir distinto." >&2
return 2
fi
fi
# ── detección del binario chromium ────────────────────────────────────────
local _bin=""
if [[ -n "$_chrome_path" ]]; then
if [[ ! -x "$_chrome_path" ]]; then
echo "create_chrome_profile: binario no encontrado o no ejecutable: ${_chrome_path}" >&2
return 1
fi
_bin="$_chrome_path"
elif [[ $_no_launch -eq 0 ]]; then
for _candidate in chromium chromium-browser google-chrome brave-browser; do
if command -v "$_candidate" &>/dev/null; then
_bin="$_candidate"
break
fi
done
if [[ -z "$_bin" ]]; then
echo "create_chrome_profile: no se encontró binario chromium en PATH" >&2
echo " Probados: chromium, chromium-browser, google-chrome, brave-browser" >&2
echo " Usa --chrome-path o --no-launch." >&2
return 1
fi
fi
# ── modo dry-run ──────────────────────────────────────────────────────────
if [[ $_dry_run -eq 1 ]]; then
echo "=== create_chrome_profile DRY-RUN ===" >&2
echo " user-data-dir : ${_udd}" >&2
echo " profile : ${_profile_dir}" >&2
echo " name : ${_name}" >&2
if [[ $_no_launch -eq 1 ]]; then
echo " modo : --no-launch (sin chromium)" >&2
echo " acciones : mkdir -p ${_profile_path}" >&2
echo " editar ${_local_state} → info_cache + profiles_order" >&2
else
echo " binario : ${_bin}" >&2
echo " puerto CDP : ${_port}" >&2
echo " timeout : ${_timeout_sec}s" >&2
echo " acciones : systemd-run unit=create-prof-<rand> chromium headless" >&2
echo " poll Preferences hasta ${_timeout_sec}s" >&2
echo " systemctl --user stop unit" >&2
echo " editar ${_local_state} → info_cache + profiles_order" >&2
fi
printf '{"profile":"%s","name":"%s","launched":false,"preferences_created":false,"dry_run":true}\n' \
"$_profile_dir" "$_name"
return 0
fi
# ── crear directorio del perfil ───────────────────────────────────────────
mkdir -p "$_profile_path"
# ── también asegurar que user-data-dir existe ──────────────────────────────
mkdir -p "$_udd"
# ── modo --no-launch: solo estructura + Local State ────────────────────────
local _launched=false
local _prefs_created=false
if [[ $_no_launch -eq 1 ]]; then
_update_local_state "$_udd" "$_local_state" "$_profile_dir" "$_name"
if [[ -f "$_prefs_file" ]]; then
_prefs_created=true
fi
printf '{"profile":"%s","name":"%s","launched":false,"preferences_created":%s}\n' \
"$_profile_dir" "$_name" "$_prefs_created"
return 0
fi
# ── lanzar chromium headless vía systemd-run ──────────────────────────────
# systemd-run --user aísla el proceso del cgroup del agente (evita exit-144).
# NO se pasa --disable-extensions para que la managed policy instale las
# extensiones force-listed (uBlock, web_proxy).
local _rand
_rand="$(tr -dc 'a-z0-9' </dev/urandom | head -c 8 2>/dev/null || echo "$$")"
local _unit="create-prof-${_rand}"
systemd-run \
--user \
--collect \
--unit="$_unit" \
--setenv=DISPLAY=:0 \
--setenv=XAUTHORITY="${HOME}/.Xauthority" \
"$_bin" \
"--user-data-dir=${_udd}" \
"--profile-directory=${_profile_dir}" \
"--headless=new" \
"--no-first-run" \
"--remote-debugging-port=${_port}" \
"--remote-allow-origins=*" \
"about:blank" 2>/dev/null || true
_launched=true
# ── poll: esperar a que Preferences exista ────────────────────────────────
local _elapsed=0
while [[ $_elapsed -lt $_timeout_sec ]]; do
if [[ -f "$_prefs_file" ]]; then
_prefs_created=true
break
fi
sleep 1
(( _elapsed++ )) || true
done
# ── detener el unit Y matar TODO el árbol de chromium de este udd ───────────
# Necesario para poder editar Local State sin que Chrome lo sobreescriba. Ni el
# `systemctl stop` ni un `pkill -f --user-data-dir=` bastan: los procesos hijos
# (zygote/gpu/renderer) no repiten el flag --user-data-dir pero sí referencian la
# ruta del user-data-dir en otros argumentos. Los matamos por PID seleccionando
# los procesos chromium cuyo cmdline contiene la ruta del udd (seguro: no mata
# este propio script porque filtramos por '[c]hromium').
systemctl --user kill -s SIGKILL "$_unit" 2>/dev/null || true
systemctl --user stop "$_unit" 2>/dev/null || true
# Matar por PID los procesos cuyo comm es exactamente "chromium" (pgrep -x) y cuyo cmdline
# contiene la ruta del udd. Usamos pgrep -x para NO auto-matchear grep/pgrep: el path del udd
# contiene la cadena "chromium" (~/.config/chromium-cdp).
local _wait=0 _p _pids
while :; do
_pids=""
for _p in $(pgrep -x chromium 2>/dev/null); do
tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_udd" && _pids="$_pids $_p"
done
[[ -z "${_pids// }" ]] && break
# shellcheck disable=SC2086
kill -TERM $_pids 2>/dev/null || true
sleep 0.5
(( _wait++ )) || true
if [[ $_wait -ge 20 ]]; then
# shellcheck disable=SC2086
kill -9 $_pids 2>/dev/null || true
break
fi
done
rm -f "${_udd}/SingletonLock" 2>/dev/null || true
if [[ "$_prefs_created" == false ]]; then
echo "create_chrome_profile: timeout (${_timeout_sec}s) esperando a que se cree: ${_prefs_file}" >&2
echo " El directorio del perfil puede existir pero está vacío." >&2
printf '{"profile":"%s","name":"%s","launched":true,"preferences_created":false,"error":"timeout"}\n' \
"$_profile_dir" "$_name"
return 3
fi
# ── editar Local State para asignar nombre legible ────────────────────────
_update_local_state "$_udd" "$_local_state" "$_profile_dir" "$_name"
printf '{"profile":"%s","name":"%s","launched":true,"preferences_created":true}\n' \
"$_profile_dir" "$_name"
}
# ── helper: editar Local State con python3 ────────────────────────────────────
# Crea/actualiza info_cache.<profile_dir> con name + is_using_default_name=false
# y añade profile_dir a profiles_order si no está.
_update_local_state() {
local _udd="$1"
local _local_state="$2"
local _profile_dir="$3"
local _name="$4"
local _today
_today="$(date +%Y%m%d)"
# Si Local State no existe, crear una estructura mínima
if [[ ! -f "$_local_state" ]]; then
printf '{"profile":{"info_cache":{},"profiles_order":[]}}\n' > "$_local_state"
fi
# Backup antes de modificar (no sobreescribir el del mismo día)
local _backup="${_local_state}.bak.${_today}"
if [[ ! -f "$_backup" ]]; then
cp "$_local_state" "$_backup"
fi
# Editar con python3
if ! python3 - "$_local_state" "$_profile_dir" "$_name" <<'PY'; then
import sys, json
ls_path = sys.argv[1]
prof_dir = sys.argv[2]
prof_name = sys.argv[3]
with open(ls_path, "r", encoding="utf-8") as f:
data = json.load(f)
# Asegurar estructura profile
profile_section = data.setdefault("profile", {})
info_cache = profile_section.setdefault("info_cache", {})
# Crear o actualizar la entrada del perfil en info_cache
entry = info_cache.setdefault(prof_dir, {})
entry["name"] = prof_name
entry["is_using_default_name"] = False
# Añadir a profiles_order si no está
order = profile_section.setdefault("profiles_order", [])
if prof_dir not in order:
order.append(prof_dir)
with open(ls_path, "w", encoding="utf-8") as f:
json.dump(data, f, separators=(",", ":"))
PY
echo "create_chrome_profile: error editando Local State con python3" >&2
return 4
fi
# Validar JSON tras escritura
if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then
echo "create_chrome_profile: JSON inválido tras escribir Local State; restaurando backup" >&2
cp "$_backup" "$_local_state"
return 4
fi
}
# ── auto-ejecución ────────────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
create_chrome_profile "$@"
fi
@@ -0,0 +1,93 @@
---
name: delete_chrome_profile
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "delete_chrome_profile --user-data-dir <dir> --profile <name> [--profile <name>]... [--dry-run]"
description: "Borra por completo uno o varios perfiles Chrome/Chromium: elimina la carpeta del perfil del disco y limpia todas sus referencias en Local State (info_cache, profiles_order, last_active_profiles, last_used, variations_google_groups). Requiere que Chromium esté cerrado. Hace backup automático de Local State antes de editar y valida el JSON resultante restaurando el backup si es inválido."
tags: [navegator, chromium, profile, cleanup, browser, scraping]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/delete_chrome_profile.sh"
params:
- name: --user-data-dir
desc: "Ruta raíz del user-data-dir de Chrome/Chromium (obligatorio). Ej: ~/.config/chromium"
- name: --profile
desc: "Nombre de la carpeta del perfil a borrar (repetible, mínimo uno obligatorio). Ej: 'Default', 'Profile 1'"
- name: --dry-run
desc: "Muestra qué carpetas borraría y qué claves de Local State quitaría sin tocar nada. No activa el guard de chromium cerrado."
output: "JSON en stdout. Modo real: {deleted:[{profile, dir_removed, local_state_cleaned}...], last_used:'<nuevo>', backup:'Local State.bak.YYYYMMDD'}. Modo dry-run: {dry_run:true, would_delete:[{profile, dir_exists, would_remove, local_state_would_clean}...]}. Errores a stderr con exit != 0."
---
## Ejemplo
```bash
# Cerrar Chromium primero (OBLIGATORIO en modo real)
pkill -TERM chromium
# Borrar un perfil
source $HOME/fn_registry/bash/functions/browser/delete_chrome_profile.sh
delete_chrome_profile \
--user-data-dir "$HOME/.config/chromium" \
--profile "Profile 1"
# Salida: {"deleted":[{"profile":"Profile 1","dir_removed":true,"local_state_cleaned":true}],"last_used":"Default","backup":"Local State.bak.20260606"}
# Borrar varios perfiles a la vez
delete_chrome_profile \
--user-data-dir "$HOME/.config/chromium" \
--profile "Profile 1" \
--profile "Profile 2"
# Previsualizar sin tocar nada (no requiere Chromium cerrado)
delete_chrome_profile \
--user-data-dir "$HOME/.config/chromium" \
--profile "Profile 1" \
--dry-run
# Salida: {"dry_run":true,"would_delete":[{"profile":"Profile 1","dir_exists":true,"would_remove":true,"local_state_would_clean":true}]}
# Con un user-data-dir sintético para pruebas
mkdir -p /tmp/test_udd/Default /tmp/test_udd/"Profile 1"
echo '{"profile":{"info_cache":{"Default":{},"Profile 1":{}},"profiles_order":["Default","Profile 1"],"last_active_profiles":["Profile 1"],"last_used":"Profile 1"},"variations_google_groups":{}}' \
> "/tmp/test_udd/Local State"
delete_chrome_profile --user-data-dir /tmp/test_udd --profile "Profile 1" --dry-run
```
También ejecutable directamente con `fn run`:
```bash
cd $HOME/fn_registry
./fn run delete_chrome_profile_bash_browser -- \
--user-data-dir "$HOME/.config/chromium" --profile "Profile 1" --dry-run
```
## Cuando usarla
Úsala cuando necesites limpiar completamente un perfil de Chromium: antes de crear un perfil de scraping fresco, para depurar problemas de perfiles corruptos, o para liberar espacio eliminando perfiles de sesión temporales. A diferencia de borrar solo la carpeta, esta función también retira las referencias de `Local State` para que Chromium no muestre el perfil fantasma ni intente acceder a él al arrancar.
## Gotchas
- **Chromium DEBE estar cerrado antes de ejecutar en modo real**. Chromium reescribe `Local State` desde memoria al cerrar y desharía todos los cambios. La función comprueba `pgrep -x chromium` y aborta con exit 2 si detecta procesos vivos. En `--dry-run` este check no se activa.
- **Operación destructiva e irreversible**: todos los datos del perfil (cookies, logins guardados, historial, caché, contraseñas) se pierden permanentemente al borrar la carpeta. No hay papelera.
- **Backup automático de Local State**: antes de editar, la función crea `<udd>/Local State.bak.YYYYMMDD`. Si ya existe un backup del día no lo sobreescribe. Restaurar manualmente: `cp "Local State.bak.YYYYMMDD" "Local State"`.
- **Validación JSON tras edición**: si el JSON de Local State queda inválido (raro pero posible con perfiles con nombres muy especiales), la función restaura el backup automáticamente y sale con exit != 0.
- **Nombres de perfil con espacios**: los nombres como `"Profile 1"` se pasan entre comillas al script Python. El parsing usa `json.loads` por lo que los espacios no dan problemas, pero deben pasarse correctamente en el shell: `--profile "Profile 1"`.
- **python3 > jq > warning**: usa python3 para editar Local State, jq como fallback. Si ninguno está disponible, las carpetas se borran pero Local State queda sin modificar (Chromium podría mostrar perfiles fantasma al arrancar).
- **last_used reasignado automáticamente**: si el perfil borrado era el `last_used`, la función asigna el primer perfil restante en `info_cache`. Si no queda ningún perfil, `last_used` queda como cadena vacía.
- **No afecta a `--profile Default` si es el único perfil**: lo borrará igualmente — Chromium puede quedar sin ningún perfil configurado y recreará Default al arrancar.
## Exit codes
| Código | Significado |
|--------|-------------|
| 0 | Éxito o dry-run completado |
| 1 | Argumento inválido, directorio o Local State no encontrado, JSON inválido tras edición |
| 2 | Chromium está corriendo (solo en modo real) |
@@ -0,0 +1,264 @@
#!/usr/bin/env bash
# delete_chrome_profile — borra por completo uno o varios perfiles Chrome/Chromium:
# elimina la carpeta del perfil y limpia todas las referencias en Local State
# (info_cache, profiles_order, last_active_profiles, last_used, variations_google_groups).
set -euo pipefail
delete_chrome_profile() {
# ── defaults ──────────────────────────────────────────────────────────────
local _user_data_dir=""
local _profiles=()
local _dry_run=0
# ── parse args ─────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: delete_chrome_profile --user-data-dir <dir> --profile <name> [--profile <name>]... [--dry-run]
--user-data-dir <dir> Ruta raíz del user-data-dir de Chrome/Chromium (obligatorio).
--profile <name> Nombre de la carpeta del perfil, ej. "Default" o "Profile 1"
(repetible, al menos uno obligatorio).
--dry-run Muestra qué borraría y qué claves de Local State quitaría
sin tocar nada.
Exit codes:
0 éxito (o dry-run completado)
1 error de argumento o validación
2 chromium está corriendo (solo en modo real)
EOF
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _user_data_dir="$2"; shift 2 ;;
--profile) _profiles+=("$2"); shift 2 ;;
--dry-run) _dry_run=1; shift ;;
-h|--help) _usage; return 0 ;;
*) echo "delete_chrome_profile: argumento desconocido: $1" >&2; return 1 ;;
esac
done
# ── validaciones de argumentos ────────────────────────────────────────────
if [[ -z "$_user_data_dir" ]]; then
echo "delete_chrome_profile: --user-data-dir es obligatorio" >&2
return 1
fi
if [[ ${#_profiles[@]} -eq 0 ]]; then
echo "delete_chrome_profile: se requiere al menos un --profile" >&2
return 1
fi
if [[ ! -d "$_user_data_dir" ]]; then
echo "delete_chrome_profile: user-data-dir no encontrado: ${_user_data_dir}" >&2
return 1
fi
local _local_state="${_user_data_dir}/Local State"
if [[ ! -f "$_local_state" ]]; then
echo "delete_chrome_profile: Local State no encontrado: ${_local_state}" >&2
return 1
fi
# ── guard: ningún chromium debe tener ESTE user-data-dir abierto (excepto en dry-run) ──
# Por-udd, no global. Comprobamos por PID con comm=chromium (pgrep -x) y leemos su cmdline,
# para NO auto-matchear el propio `grep`/`pgrep` del pipe: como el path del udd contiene la
# cadena "chromium" (p.ej. ~/.config/chromium-cdp), un `pgrep -af '[c]hromium' | grep <udd>`
# se detecta a sí mismo. pgrep -x chromium solo lista procesos cuyo nombre es exactamente
# "chromium" (el navegador), nunca grep/pgrep/bash.
if [[ $_dry_run -eq 0 ]]; then
local _p _busy=0
for _p in $(pgrep -x chromium 2>/dev/null); do
if tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_user_data_dir"; then
_busy=1; break
fi
done
if [[ $_busy -eq 1 ]]; then
echo "delete_chrome_profile: hay un chromium con este user-data-dir abierto — ciérralo antes de borrar perfiles:" >&2
echo " pkill -TERM chromium" >&2
echo "(Chromium reescribe Local State desde memoria al cerrar y desharía el borrado)" >&2
return 2
fi
fi
local _today
_today="$(date +%Y%m%d)"
# ── modo dry-run ──────────────────────────────────────────────────────────
if [[ $_dry_run -eq 1 ]]; then
echo "=== delete_chrome_profile DRY-RUN ===" >&2
local _p
for _p in "${_profiles[@]}"; do
local _pdir="${_user_data_dir}/${_p}"
if [[ -d "$_pdir" ]]; then
echo " [borraría] rm -rf ${_pdir}" >&2
else
echo " [no existe] ${_pdir}" >&2
fi
echo " [Local State] quitaría claves para perfil: '${_p}'" >&2
echo " profile.info_cache.${_p}" >&2
echo " profile.profiles_order (entrada '${_p}')" >&2
echo " profile.last_active_profiles (entrada '${_p}')" >&2
echo " profile.last_used (si == '${_p}', reasignar)" >&2
echo " variations_google_groups.${_p} (si existe)" >&2
done
# Construir JSON de dry-run inline
local _dry_items="" _dry_first=1
for _p in "${_profiles[@]}"; do
local _pdir="${_user_data_dir}/${_p}"
local _sep="" _exists="false"
[[ $_dry_first -eq 0 ]] && _sep=","
_dry_first=0
[[ -d "$_pdir" ]] && _exists="true"
_dry_items+="${_sep}{\"profile\":\"${_p}\",\"dir_exists\":${_exists},\"would_remove\":${_exists},\"local_state_would_clean\":true}"
done
printf '{"dry_run":true,"would_delete":[%s]}\n' "$_dry_items"
return 0
fi
# ── backup de Local State (no sobreescribir el del día) ───────────────────
local _backup="${_local_state}.bak.${_today}"
if [[ ! -f "$_backup" ]]; then
cp "$_local_state" "$_backup"
fi
# ── borrar carpetas de perfil ──────────────────────────────────────────────
local _deleted_results=() # "profile|dir_removed|ls_cleaned"
local _p
for _p in "${_profiles[@]}"; do
local _pdir="${_user_data_dir}/${_p}"
local _dir_removed=false
if [[ -d "$_pdir" ]]; then
rm -rf "$_pdir"
_dir_removed=true
fi
_deleted_results+=("${_p}|${_dir_removed}|false")
done
# ── construir lista Python de perfiles a eliminar ─────────────────────────
local _py_profiles_list=""
for _p in "${_profiles[@]}"; do
_py_profiles_list+="\"${_p}\","
done
_py_profiles_list="[${_py_profiles_list%,}]"
# ── editar Local State con python3 ────────────────────────────────────────
local _ls_cleaned=false
if command -v python3 >/dev/null 2>&1; then
python3 - "$_local_state" "$_py_profiles_list" <<'PY'
import sys, json
ls_path = sys.argv[1]
profiles_to_delete = json.loads(sys.argv[2])
with open(ls_path, "r", encoding="utf-8") as f:
data = json.load(f)
profile_section = data.get("profile", {})
# 1. profile.info_cache — eliminar cada perfil
info_cache = profile_section.get("info_cache", {})
for p in profiles_to_delete:
info_cache.pop(p, None)
# 2. profile.profiles_order — quitar entradas del perfil
if "profiles_order" in profile_section and isinstance(profile_section["profiles_order"], list):
profile_section["profiles_order"] = [
x for x in profile_section["profiles_order"] if x not in profiles_to_delete
]
# 3. profile.last_active_profiles — quitar entradas del perfil
if "last_active_profiles" in profile_section and isinstance(profile_section["last_active_profiles"], list):
profile_section["last_active_profiles"] = [
x for x in profile_section["last_active_profiles"] if x not in profiles_to_delete
]
# 4. profile.last_used — reasignar si apunta a un perfil borrado
last_used = profile_section.get("last_used", "")
if last_used in profiles_to_delete:
remaining = [k for k in info_cache.keys() if k not in profiles_to_delete]
profile_section["last_used"] = remaining[0] if remaining else ""
# 5. variations_google_groups — limpiar entradas del perfil (si existe)
vgg = data.get("variations_google_groups", {})
for p in profiles_to_delete:
vgg.pop(p, None)
with open(ls_path, "w", encoding="utf-8") as f:
json.dump(data, f, separators=(",", ":"))
PY
_ls_cleaned=true
# ── fallback con jq ───────────────────────────────────────────────────────
elif command -v jq >/dev/null 2>&1; then
local _tmp_ls
_tmp_ls="$(mktemp)"
local _jq_expr="."
for _p in "${_profiles[@]}"; do
_jq_expr+=" | del(.profile.info_cache[\"${_p}\"])"
_jq_expr+=" | del(.variations_google_groups[\"${_p}\"])"
_jq_expr+=" | if .profile.profiles_order then .profile.profiles_order -= [\"${_p}\"] else . end"
_jq_expr+=" | if .profile.last_active_profiles then .profile.last_active_profiles -= [\"${_p}\"] else . end"
done
if jq "${_jq_expr}" "$_local_state" > "$_tmp_ls" 2>/dev/null; then
mv "$_tmp_ls" "$_local_state"
_ls_cleaned=true
else
echo "delete_chrome_profile: advertencia — jq falló editando Local State" >&2
rm -f "$_tmp_ls"
fi
else
echo "delete_chrome_profile: advertencia — ni python3 ni jq disponibles; carpetas borradas pero Local State no modificado" >&2
fi
# ── validar que el JSON resultante sigue siendo parseable ─────────────────
if [[ "$_ls_cleaned" == "true" ]]; then
if command -v python3 >/dev/null 2>&1; then
if ! python3 -c "import sys, json; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then
echo "delete_chrome_profile: JSON de Local State inválido tras edición — restaurando backup" >&2
cp "$_backup" "$_local_state"
return 1
fi
fi
fi
# ── actualizar _deleted_results con ls_cleaned ────────────────────────────
local _updated_results=()
for _entry in "${_deleted_results[@]}"; do
local _ep _edr _els
IFS='|' read -r _ep _edr _els <<< "$_entry"
_updated_results+=("${_ep}|${_edr}|${_ls_cleaned}")
done
# ── leer last_used resultante ──────────────────────────────────────────────
local _new_last_used=""
if command -v python3 >/dev/null 2>&1; then
_new_last_used="$(python3 -c "
import sys, json
data = json.load(open(sys.argv[1]))
print(data.get('profile', {}).get('last_used', ''))
" "$_local_state" 2>/dev/null || echo "")"
fi
# ── construir JSON de resultado inline ────────────────────────────────────
local _result_items="" _res_first=1
for _entry in "${_updated_results[@]+"${_updated_results[@]}"}"; do
local _pn _dr _lc
IFS='|' read -r _pn _dr _lc <<< "$_entry"
local _rsep=""
[[ $_res_first -eq 0 ]] && _rsep=","
_res_first=0
_result_items+="${_rsep}{\"profile\":\"${_pn}\",\"dir_removed\":${_dr},\"local_state_cleaned\":${_lc}}"
done
printf '{"deleted":[%s],"last_used":"%s","backup":"Local State.bak.%s"}\n' \
"$_result_items" "$_new_last_used" "$_today"
}
# ── auto-ejecución ────────────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
delete_chrome_profile "$@"
fi
@@ -0,0 +1,93 @@
---
name: restore_chrome_bookmarks
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "restore_chrome_bookmarks --backup-dir <ts-dir> [--user-data-dir <dir>] [--profile <name>]... [--dry-run]"
description: "Restaura archivos Bookmarks de Chrome/Chromium desde un directorio de backup generado por backup_chrome_bookmarks hacia los perfiles destino en user-data-dir. Copia byte a byte con cp -p para preservar el checksum MD5 interno del archivo. Nunca parsea ni reserializa el JSON. Requiere que Chromium esté cerrado antes de ejecutar."
tags: [navegator, chromium, bookmarks, restore, browser, scraping, profile]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/restore_chrome_bookmarks.sh"
params:
- name: --backup-dir
desc: "Directorio de backup con timestamp generado por backup_chrome_bookmarks. Debe contener subdirectorios <profile>/Bookmarks. OBLIGATORIO."
- name: --user-data-dir
desc: "Ruta raíz del user-data-dir de Chrome/Chromium destino. Default: ~/.config/chromium"
- name: --profile
desc: "Nombre del perfil a restaurar (repetible, ej. Default, Profile 1). Si no se pasa ninguno se restauran TODOS los perfiles presentes en el backup-dir."
- name: --dry-run
desc: "Muestra qué archivos se copiarían y cuáles Bookmarks.bak se borrarían, sin tocar nada en disco."
output: "JSON en stdout: {\"restored\": [{\"profile\": \"Default\", \"dst\": \"<path>\", \"bytes\": N}, ...]}. Exit 0 en éxito o dry-run. Errores a stderr con exit != 0."
---
## Ejemplo
```bash
# PASO 1 — cerrar Chromium (OBLIGATORIO en modo real)
pkill -TERM chromium
# PASO 2 — restaurar todos los perfiles desde el backup más reciente
source $HOME/fn_registry/bash/functions/browser/restore_chrome_bookmarks.sh
restore_chrome_bookmarks \
--user-data-dir "$HOME/.config/chromium" \
--backup-dir "$HOME/backups/chromium_bookmarks/2026-06-04T15:30:00"
# Restaurar solo un perfil concreto
restore_chrome_bookmarks \
--backup-dir "$HOME/backups/chromium_bookmarks/2026-06-04T15:30:00" \
--profile Default
# Restaurar dos perfiles específicos
restore_chrome_bookmarks \
--backup-dir "$HOME/backups/chromium_bookmarks/2026-06-04T15:30:00" \
--profile Default \
--profile "Profile 1"
# Previsualizar sin tocar nada (no necesita Chromium cerrado)
restore_chrome_bookmarks \
--backup-dir "$HOME/backups/chromium_bookmarks/2026-06-04T15:30:00" \
--dry-run
# Salida esperada:
# {"restored":[{"profile":"Default","dst":"/home/enmanuel/.config/chromium/Default/Bookmarks","bytes":12453}]}
```
También ejecutable directamente con `fn run`:
```bash
cd $HOME/fn_registry
./fn run restore_chrome_bookmarks_bash_browser -- \
--backup-dir "$HOME/backups/chromium_bookmarks/2026-06-04T15:30:00" \
--dry-run
```
## Cuando usarla
Úsala después de una sesión de scraping o automatización que haya alterado los bookmarks, o para recuperar bookmarks tras formatear/recrear un perfil de Chromium. Combínala con `backup_chrome_bookmarks` (que genera el `--backup-dir` con la estructura esperada) para tener un ciclo completo de backup/restore. También útil para propagar bookmarks de un perfil o PC a otro.
## Gotchas
- **Chromium DEBE estar cerrado** antes de ejecutar en modo real. Chromium mantiene los bookmarks en memoria y los reescribe al archivo `Bookmarks` al cerrar; si restauras con Chromium abierto, el proceso sobreescribirá tu restauración al cerrarse. La función lo comprueba con `pgrep -x chromium` y aborta con exit 2 si hay procesos vivos. En `--dry-run` este check se omite.
- **Copia verbatim — nunca reserializar el JSON**. El archivo `Bookmarks` contiene un campo `checksum` con el MD5 del propio contenido JSON (calculado por Chromium internamente). Si se parsea y reserializa el JSON (aunque sea equivalente), el checksum queda inválido y Chromium descarta silenciosamente el archivo y regenera uno vacío. Esta función usa `cp -p` para garantizar que los bytes son idénticos al original.
- **En Chromium 148 los bookmarks NO están bajo `super_mac` de Secure Preferences**. No es necesario tocar `Preferences` ni `Secure Preferences` al restaurar bookmarks (a diferencia de extensiones). La función solo opera sobre el archivo `Bookmarks`.
- **`Bookmarks.bak` residual se borra**. Chromium crea `Bookmarks.bak` como copia de seguridad interna. Si existe antes de la restauración, esta función lo borra para que Chromium no lo use como fallback en lugar del archivo recién restaurado.
- **El directorio destino del perfil se crea si no existe**. Si el perfil aún no tiene directorio en `user-data-dir`, se crea con `mkdir -p`. Chromium lo inicializará correctamente la primera vez que arranque con ese perfil.
- **Opera por perfil**. Si no pasas `--profile`, restaura todos los perfiles presentes en el backup. Pasa `--profile` explícito para restaurar selectivamente y evitar sobreescribir perfiles sin querer.
## Exit codes
| Código | Significado |
|--------|------------|
| 0 | Éxito o dry-run completado |
| 1 | Argumento inválido, backup-dir/user-data-dir no encontrado, o perfil no presente en backup |
| 2 | Chromium está corriendo (solo en modo real) |
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
# restore_chrome_bookmarks — restaura archivos Bookmarks de un backup generado por
# backup_chrome_bookmarks hacia los perfiles destino en user-data-dir.
# Copia byte a byte con cp -p (nunca parsea ni reserializa el JSON).
set -euo pipefail
restore_chrome_bookmarks() {
# ── defaults ──────────────────────────────────────────────────────────────
local _user_data_dir="${HOME}/.config/chromium"
local _backup_dir=""
local _profiles=()
local _dry_run=0
# ── parse args ────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: restore_chrome_bookmarks --backup-dir <ts-dir>
[--user-data-dir <dir>] [--profile <name>]... [--dry-run]
--user-data-dir Raíz de perfiles destino. Default: ~/.config/chromium
--backup-dir Directorio de backup con timestamp generado por
backup_chrome_bookmarks. Debe contener subdirectorios
<profile>/Bookmarks. OBLIGATORIO.
--profile <name> Perfil a restaurar (repetible). Si no se pasa ninguno
se restauran TODOS los perfiles presentes en backup-dir.
--dry-run Muestra qué se copiaría sin tocar nada.
Exit codes:
0 éxito (o dry-run completado)
1 error de argumento o validación
2 chromium está corriendo (solo en modo real)
EOF
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _user_data_dir="$2"; shift 2 ;;
--backup-dir) _backup_dir="$2"; shift 2 ;;
--profile) _profiles+=("$2"); shift 2 ;;
--dry-run) _dry_run=1; shift ;;
-h|--help) _usage; return 0 ;;
*) echo "restore_chrome_bookmarks: argumento desconocido: $1" >&2; return 1 ;;
esac
done
# ── validaciones ──────────────────────────────────────────────────────────
if [[ -z "$_backup_dir" ]]; then
echo "restore_chrome_bookmarks: --backup-dir es obligatorio" >&2
return 1
fi
if [[ ! -d "$_backup_dir" ]]; then
echo "restore_chrome_bookmarks: backup-dir no encontrado: ${_backup_dir}" >&2
return 1
fi
if [[ ! -d "$_user_data_dir" ]]; then
echo "restore_chrome_bookmarks: user-data-dir no encontrado: ${_user_data_dir}" >&2
return 1
fi
# ── guard: ningún chromium debe tener ESTE user-data-dir abierto (excepto en dry-run) ──
# Por-udd, no global. Comprobamos por PID con comm=chromium (pgrep -x) y leemos su cmdline,
# para NO auto-matchear el propio `grep`/`pgrep`: el path del udd contiene "chromium"
# (~/.config/chromium-cdp), así que un `pgrep -af '[c]hromium' | grep <udd>` se detecta a sí mismo.
if [[ $_dry_run -eq 0 ]]; then
local _p _busy=0
for _p in $(pgrep -x chromium 2>/dev/null); do
if tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_user_data_dir"; then
_busy=1; break
fi
done
if [[ $_busy -eq 1 ]]; then
echo "restore_chrome_bookmarks: hay un chromium con este user-data-dir abierto — ciérralo antes de restaurar:" >&2
echo " pkill -TERM chromium" >&2
echo "(Chromium reescribe Bookmarks desde memoria al cerrar y desharía la restauración)" >&2
return 2
fi
fi
# ── determinar perfiles a restaurar ───────────────────────────────────────
local _target_profiles=()
if [[ ${#_profiles[@]} -gt 0 ]]; then
# Perfiles explícitos: verificar que existen en el backup
local _p
for _p in "${_profiles[@]}"; do
if [[ ! -f "${_backup_dir}/${_p}/Bookmarks" ]]; then
echo "restore_chrome_bookmarks: backup no contiene perfil '${_p}': ${_backup_dir}/${_p}/Bookmarks" >&2
return 1
fi
_target_profiles+=("$_p")
done
else
# Autodescubrir todos los perfiles en el backup
local _profile_path
while IFS= read -r -d '' _profile_path; do
local _pname
_pname="$(basename "$(dirname "$_profile_path")")"
_target_profiles+=("$_pname")
done < <(find "$_backup_dir" -mindepth 2 -maxdepth 2 -name "Bookmarks" -print0 | sort -z)
if [[ ${#_target_profiles[@]} -eq 0 ]]; then
echo "restore_chrome_bookmarks: no se encontraron archivos Bookmarks en: ${_backup_dir}" >&2
return 1
fi
fi
# ── restaurar cada perfil ─────────────────────────────────────────────────
local _restored_json=""
local _first=1
local _prof
for _prof in "${_target_profiles[@]}"; do
local _src="${_backup_dir}/${_prof}/Bookmarks"
local _dst_dir="${_user_data_dir}/${_prof}"
local _dst="${_dst_dir}/Bookmarks"
local _dst_bak="${_dst_dir}/Bookmarks.bak"
# Tamaño del archivo fuente para el JSON de salida
local _bytes=0
if [[ -f "$_src" ]]; then
_bytes="$(wc -c < "$_src")"
# Eliminar espacios que wc puede añadir en algunas plataformas
_bytes="${_bytes// /}"
fi
if [[ $_dry_run -eq 1 ]]; then
echo "=== restore_chrome_bookmarks DRY-RUN ===" >&2
echo " Perfil : ${_prof}" >&2
echo " src : ${_src}" >&2
echo " dst : ${_dst}" >&2
echo " bytes : ${_bytes}" >&2
if [[ -f "$_dst_bak" ]]; then
echo " .bak : borraría ${_dst_bak}" >&2
fi
else
# Crear directorio destino si no existe
mkdir -p "$_dst_dir"
# Copiar byte a byte preservando timestamps (NUNCA reserializar)
cp -p "$_src" "$_dst"
# Borrar Bookmarks.bak residual si existe
if [[ -f "$_dst_bak" ]]; then
rm -f "$_dst_bak"
fi
fi
# Construir fragmento JSON para este perfil
local _entry
_entry="$(printf '{"profile":"%s","dst":"%s","bytes":%s}' \
"$_prof" "$_dst" "$_bytes")"
if [[ $_first -eq 1 ]]; then
_restored_json="${_entry}"
_first=0
else
_restored_json+=",$_entry"
fi
done
# ── emitir resultado JSON ─────────────────────────────────────────────────
printf '{"restored":[%s]}\n' "$_restored_json"
}
# ── auto-ejecución ────────────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
restore_chrome_bookmarks "$@"
fi
@@ -0,0 +1,67 @@
---
name: reset_chrome_profiles
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "reset_chrome_profiles --user-data-dir <dir> [--profile \"<dir>=<legible>\"]... [--backup-dir <dir>] [--base-port 9250] [--keep <ext_id>]... [--dry-run] [--yes]"
description: "Pipeline de reset destructivo de perfiles de Chromium: hace backup de los bookmarks de todos los perfiles, cierra el chromium que use ese user-data-dir, borra los perfiles (carpeta + Local State), los recrea (la managed policy reinstala la whitelist de extensiones uBlock + web_proxy), restaura los bookmarks y verifica que cada perfil quedó solo con la whitelist. DESTRUCTIVO: se pierden cookies, logins, historial y contraseñas; solo los bookmarks se preservan. Requiere --yes en modo real."
tags: [launcher, navegator, chromium, pipeline, profile, reset]
uses_functions:
- backup_chrome_bookmarks_bash_browser
- delete_chrome_profile_bash_browser
- create_chrome_profile_bash_browser
- restore_chrome_bookmarks_bash_browser
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--user-data-dir <dir>"
desc: "Raíz del user-data-dir de Chromium cuyos perfiles se resetean (ej. ~/.config/chromium-cdp)."
- name: "--profile <dir=legible>"
desc: "Perfil a resetear, formato carpeta=nombre-legible (repetible). Default los 4 reales: Default=Work, Personal=Personal, 'Profile 1'=Aurgi, Automation=Automation."
- name: "--backup-dir <dir>"
desc: "Directorio donde se guardan los backups de bookmarks. Default ~/.local/share/web_scraping/bookmarks-backups."
- name: "--base-port <N>"
desc: "Puerto CDP base para recrear perfiles (cada perfil usa base+i). Default 9250."
- name: "--keep <ext_id>"
desc: "ID de extensión esperada tras el reset (repetible). Default uBlock Origin Lite + web_proxy toggle. Solo se usa en la verificación final."
- name: "--dry-run"
desc: "Previsualiza los 6 pasos sin tocar el sistema."
- name: "--yes"
desc: "Confirma la operación destructiva (obligatorio en modo real)."
output: "Ejecuta backup → cerrar chromium → delete → create → restore → verify. Emite el progreso de cada paso y un resumen. Sale 0 si todo OK y cada perfil quedó solo con la whitelist; != 0 si falla algún paso o la verificación detecta extensiones fuera de la whitelist."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/reset_chrome_profiles.sh"
---
## Ejemplo
```bash
# Previsualizar el reset de los 4 perfiles del chromium diario (no toca nada)
fn run reset_chrome_profiles --user-data-dir "$HOME/.config/chromium-cdp" --dry-run
# Reset real (destructivo): backup bookmarks, borrar+recrear los 4 perfiles, restaurar bookmarks
fn run reset_chrome_profiles --user-data-dir "$HOME/.config/chromium-cdp" --yes
# Reset de un solo perfil con nombre legible
fn run reset_chrome_profiles --user-data-dir "$HOME/.config/chromium-cdp" \
--profile "Automation=Automation" --yes
```
## Cuando usarla
Cuando quieras dejar los perfiles de un Chromium **limpios desde cero** conservando solo la whitelist de extensiones (uBlock + la de captura del web_proxy) y preservando los bookmarks, pero descartando todo el resto del estado (cookies, logins, historial). Útil para volver a un estado conocido de scraping/captura o para limpiar perfiles contaminados. La managed policy de `/etc` ya fuerza la whitelist, así que los perfiles recreados nacen correctos.
## Gotchas
- **DESTRUCTIVO**: cookies, logins, historial y contraseñas de los perfiles se pierden de forma irreversible. Solo los bookmarks se preservan (backup + restore byte a byte). Por eso requiere `--yes` en modo real.
- **Cierra el chromium del user-data-dir indicado** (pkill por `--user-data-dir`), no cualquier chromium. Si tienes otro chromium con otro user-data-dir, no se toca.
- **Depende de la managed policy**: los perfiles recreados solo tendrán uBlock + web_proxy si la policy de `/etc/chromium/policies/managed/extensions.json` las fuerza (ver `apply_chromium_extension_policy_bash_browser`). Si la policy no está, los perfiles nacen sin extensiones.
- La verificación final comprueba las carpetas en `<profile>/Extensions/`; para una auditoría detallada (nombre, versión, enabled, fromPolicy) usar `list_chrome_profile_extensions_go_browser`.
- Lanzar chromium desde el Bash tool da exit-144; `create_chrome_profile` usa `systemd-run --user` internamente para evitarlo.
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
# reset_chrome_profiles — Pipeline de reset destructivo de perfiles de Chromium.
#
# Compone funciones del registry para: hacer backup de los bookmarks de todos los perfiles,
# cerrar chromium, borrar los perfiles (carpeta + entradas en Local State), recrearlos
# (la managed policy reinstala la whitelist de extensiones: uBlock + web_proxy), restaurar
# los bookmarks y verificar que cada perfil quedó solo con la whitelist.
#
# DESTRUCTIVO: borra cookies, logins, historial y contraseñas de los perfiles. Solo los
# bookmarks se preservan (backup + restore). Requiere --yes en modo real (o --dry-run).
#
# Uso:
# reset_chrome_profiles --user-data-dir <dir>
# [--profile "<dir>=<legible>"]... [--backup-dir <dir>] [--base-port 9250]
# [--keep <ext_id>]... [--dry-run] [--yes]
#
# Defaults de --profile (los 4 perfiles reales): "Default=Work" "Personal=Personal"
# "Profile 1=Aurgi" "Automation=Automation".
# Default de --keep (whitelist esperada tras el reset): uBlock Origin Lite + web_proxy toggle.
reset_chrome_profiles() {
local _udd="" _backup_dir="${HOME}/.local/share/web_scraping/bookmarks-backups"
local _base_port=9250 _dry_run=0 _yes=0
local -a _profiles=()
local -a _keep=()
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _udd="$2"; shift 2 ;;
--profile) _profiles+=("$2"); shift 2 ;;
--backup-dir) _backup_dir="$2"; shift 2 ;;
--base-port) _base_port="$2"; shift 2 ;;
--keep) _keep+=("$2"); shift 2 ;;
--dry-run) _dry_run=1; shift ;;
--yes) _yes=1; shift ;;
-h|--help)
grep '^#' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; return 0 ;;
*) echo "reset_chrome_profiles: argumento desconocido: $1" >&2; return 1 ;;
esac
done
if [[ -z "$_udd" ]]; then
echo "reset_chrome_profiles: --user-data-dir es obligatorio" >&2; return 1
fi
if [[ ${#_profiles[@]} -eq 0 ]]; then
_profiles=("Default=Work" "Personal=Personal" "Profile 1=Aurgi" "Automation=Automation")
fi
if [[ ${#_keep[@]} -eq 0 ]]; then
_keep=("ddkjiahejlhfcafbddmgiahcphecmpfh" "nanldmckabfghgdebblpfbdbhphhbnde")
fi
# Localizar las funciones del registry que componemos.
local _dir _root _browser
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_root="$(cd "$_dir/../../.." && pwd)"
_browser="$_root/bash/functions/browser"
local _f
for _f in backup_chrome_bookmarks restore_chrome_bookmarks delete_chrome_profile create_chrome_profile; do
if [[ ! -f "$_browser/$_f.sh" ]]; then
echo "reset_chrome_profiles: falta función $_f en $_browser" >&2; return 1
fi
# shellcheck disable=SC1090
source "$_browser/$_f.sh"
done
echo "=== reset_chrome_profiles ==="
echo " user-data-dir : $_udd"
echo " perfiles : ${_profiles[*]}"
echo " whitelist ext : ${_keep[*]}"
echo " backup-dir : $_backup_dir"
echo " modo : $([[ $_dry_run -eq 1 ]] && echo DRY-RUN || echo REAL)"
echo ""
# Confirmación obligatoria en modo real.
if [[ $_dry_run -eq 0 && $_yes -eq 0 ]]; then
echo "reset_chrome_profiles: operación DESTRUCTIVA (se pierden cookies/logins/historial)." >&2
echo " Repite con --yes para confirmar, o usa --dry-run para previsualizar." >&2
return 3
fi
# ── [1/6] Backup de bookmarks (solo lee; chromium puede estar abierto) ──────
echo "[1/6] Backup de bookmarks..."
local _bk_json _ts_dir
if [[ $_dry_run -eq 1 ]]; then
backup_chrome_bookmarks --user-data-dir "$_udd" --backup-dir "$_backup_dir" --dry-run
_ts_dir="<dry-run>"
else
_bk_json="$(backup_chrome_bookmarks --user-data-dir "$_udd" --backup-dir "$_backup_dir")" || {
echo "reset_chrome_profiles: backup falló" >&2; return 1; }
echo "$_bk_json"
_ts_dir="$(printf '%s' "$_bk_json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["backup_dir"]+"/"+d["ts"])')"
echo " backup en: $_ts_dir"
fi
echo ""
# ── [2/6] Cerrar chromium que tenga ESTE user-data-dir abierto ─────────────
echo "[2/6] Cerrando chromium con --user-data-dir=$_udd ..."
if [[ $_dry_run -eq 1 ]]; then
echo " (dry-run: no se cierra nada)"
else
# Por-PID con comm=chromium (pgrep -x) para no auto-matchear grep/pgrep (el path del udd
# contiene la cadena "chromium").
local _p _kpids _i=0
_kpids=""
for _p in $(pgrep -x chromium 2>/dev/null); do
tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_udd" && _kpids="$_kpids $_p"
done
if [[ -n "${_kpids// }" ]]; then
# shellcheck disable=SC2086
kill -TERM $_kpids 2>/dev/null || true
while :; do
_kpids=""
for _p in $(pgrep -x chromium 2>/dev/null); do
tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_udd" && _kpids="$_kpids $_p"
done
[[ -z "${_kpids// }" ]] && break
_i=$((_i+1)); [[ $_i -ge 20 ]] && { kill -9 $_kpids 2>/dev/null || true; break; }
sleep 0.5
done
echo " chromium cerrado."
else
echo " (no había chromium con ese user-data-dir)"
fi
fi
echo ""
# ── [3/6] Borrar perfiles (carpeta + Local State) ──────────────────────────
echo "[3/6] Borrando perfiles..."
local _del_args=() _pair _pdir
for _pair in "${_profiles[@]}"; do
_pdir="${_pair%%=*}"
_del_args+=(--profile "$_pdir")
done
if [[ $_dry_run -eq 1 ]]; then
delete_chrome_profile --user-data-dir "$_udd" "${_del_args[@]}" --dry-run
else
delete_chrome_profile --user-data-dir "$_udd" "${_del_args[@]}" || {
echo "reset_chrome_profiles: delete falló" >&2; return 1; }
fi
echo ""
# ── [4/6] Recrear perfiles (la policy reinstala la whitelist al arrancar) ───
echo "[4/6] Recreando perfiles..."
local _idx=0 _name _port
for _pair in "${_profiles[@]}"; do
_pdir="${_pair%%=*}"; _name="${_pair#*=}"; _port=$((_base_port + _idx))
if [[ $_dry_run -eq 1 ]]; then
create_chrome_profile --user-data-dir "$_udd" --profile "$_pdir" --name "$_name" --port "$_port" --dry-run
else
create_chrome_profile --user-data-dir "$_udd" --profile "$_pdir" --name "$_name" --port "$_port" || {
echo "reset_chrome_profiles: create de '$_pdir' falló" >&2; return 1; }
fi
_idx=$((_idx+1))
done
echo ""
# ── [5/6] Restaurar bookmarks ──────────────────────────────────────────────
echo "[5/6] Restaurando bookmarks..."
if [[ $_dry_run -eq 1 ]]; then
echo " (dry-run: restauraría desde el backup recién creado)"
else
restore_chrome_bookmarks --user-data-dir "$_udd" --backup-dir "$_ts_dir" || {
echo "reset_chrome_profiles: restore falló (continúo a verify)" >&2; }
fi
echo ""
# ── [6/6] Verificar extensiones por perfil (carpetas en Extensions/) ───────
echo "[6/6] Verificando extensiones (esperado: solo la whitelist)..."
if [[ $_dry_run -eq 1 ]]; then
echo " (dry-run: verificaría que cada perfil tiene solo ${_keep[*]})"
echo ""
echo "reset_chrome_profiles: DRY-RUN completado, nada se modificó."
return 0
fi
local _ok=1
for _pair in "${_profiles[@]}"; do
_pdir="${_pair%%=*}"
local _extdir="$_udd/$_pdir/Extensions"
local -a _present=()
if [[ -d "$_extdir" ]]; then
local _e
for _e in "$_extdir"/*/; do
_e="$(basename "$_e")"
[[ "$_e" == "Temp" || "$_e" == "*" ]] && continue
_present+=("$_e")
done
fi
# Comprobar que todo lo presente está en la whitelist.
local _extra=()
local _id _found
for _id in "${_present[@]}"; do
_found=0
local _k
for _k in "${_keep[@]}"; do [[ "$_id" == "$_k" ]] && _found=1; done
[[ $_found -eq 0 ]] && _extra+=("$_id")
done
if [[ ${#_extra[@]} -gt 0 ]]; then
echo "$_pdir: extensiones fuera de whitelist: ${_extra[*]}"
_ok=0
else
echo "$_pdir: ${_present[*]:-<vacío, aún sin arrancar>}"
fi
done
echo ""
if [[ $_ok -eq 1 ]]; then
echo "reset_chrome_profiles: OK — perfiles recreados, bookmarks restaurados, solo la whitelist presente."
return 0
else
echo "reset_chrome_profiles: verificación con avisos (revisar arriba)." >&2
return 1
fi
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
reset_chrome_profiles "$@"
fi
@@ -0,0 +1,165 @@
package browser
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
)
// ProfileExtension holds metadata about a single Chrome/Chromium extension
// installed in a profile.
type ProfileExtension struct {
ID string
Name string
Version string
Location string // "unpacked" | "internal" | "component" | "external_policy" | "unknown"
Enabled bool
FromPolicy bool
}
// prefExtensionEntry mirrors the relevant fields of each entry in
// extensions.settings inside a Chromium Preferences file.
type prefExtensionEntry struct {
Manifest *struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"manifest"`
Location int `json:"location"`
State int `json:"state"`
}
// locationLabel maps Chromium location integers to human-readable strings.
func locationLabel(loc int) string {
switch loc {
case 1:
return "internal"
case 4:
return "unpacked"
case 5:
return "component"
case 7:
return "external_policy_download"
case 10:
return "external_policy"
default:
return "unknown"
}
}
// isFromPolicy returns true when the location integer indicates
// the extension was installed via enterprise policy.
func isFromPolicy(loc int) bool {
return loc == 5 || loc == 7 || loc == 10
}
// fallbackManifest attempts to read Name and Version from the on-disk
// Extensions/<id>/<version>/manifest.json file. Both return values may be
// empty strings if the file cannot be read or parsed.
func fallbackManifest(extDir, id string) (name, version string) {
idDir := filepath.Join(extDir, id)
vers, err := os.ReadDir(idDir)
if err != nil {
return "", ""
}
// There may be several version directories; use the first one found.
for _, v := range vers {
if !v.IsDir() {
continue
}
mPath := filepath.Join(idDir, v.Name(), "manifest.json")
data, err := os.ReadFile(mPath)
if err != nil {
continue
}
var m struct {
Name string `json:"name"`
Version string `json:"version"`
}
if json.Unmarshal(data, &m) == nil {
return m.Name, m.Version
}
}
return "", ""
}
// ListChromeProfileExtensions reads the Preferences file of a Chrome/Chromium
// profile and returns one ProfileExtension per entry found in
// extensions.settings.
//
// userDataDir is the user-data-dir of the browser (e.g. ~/.config/chromium).
// An empty string defaults to ~/.config/chromium.
//
// profileDir is the subdirectory name of the profile inside userDataDir
// (e.g. "Default", "Profile 1").
//
// The returned slice is sorted by ID (deterministic order).
// Returns an error if the Preferences file is missing or contains invalid JSON.
func ListChromeProfileExtensions(userDataDir, profileDir string) ([]ProfileExtension, error) {
if userDataDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
userDataDir = filepath.Join(home, ".config", "chromium")
}
prefPath := filepath.Join(userDataDir, profileDir, "Preferences")
data, err := os.ReadFile(prefPath)
if err != nil {
return nil, fmt.Errorf("list_chrome_profile_extensions: cannot read Preferences for profile %q: %w", profileDir, err)
}
// We only need extensions.settings; unmarshal into a minimal shape.
var prefs struct {
Extensions struct {
Settings map[string]prefExtensionEntry `json:"settings"`
} `json:"extensions"`
}
if err := json.Unmarshal(data, &prefs); err != nil {
return nil, fmt.Errorf("list_chrome_profile_extensions: invalid JSON in Preferences for profile %q: %w", profileDir, err)
}
extDir := filepath.Join(userDataDir, profileDir, "Extensions")
var result []ProfileExtension
for id, entry := range prefs.Extensions.Settings {
name := ""
version := ""
if entry.Manifest != nil {
name = entry.Manifest.Name
version = entry.Manifest.Version
}
// Fallback: try to read from the on-disk manifest.json.
if name == "" || version == "" {
fbName, fbVer := fallbackManifest(extDir, id)
if name == "" {
name = fbName
}
if version == "" {
version = fbVer
}
}
// state 1 = enabled, 0 = disabled; absent field defaults to enabled.
enabled := entry.State != 0
result = append(result, ProfileExtension{
ID: id,
Name: name,
Version: version,
Location: locationLabel(entry.Location),
Enabled: enabled,
FromPolicy: isFromPolicy(entry.Location),
})
}
sort.Slice(result, func(i, j int) bool {
return result[i].ID < result[j].ID
})
return result, nil
}
@@ -0,0 +1,75 @@
---
name: list_chrome_profile_extensions
kind: function
lang: go
domain: browser
version: "1.0.0"
purity: impure
signature: "func ListChromeProfileExtensions(userDataDir, profileDir string) ([]ProfileExtension, error)"
description: "Lee las extensiones instaladas en un perfil de Chrome/Chromium parseando extensions.settings del archivo Preferences. Devuelve ID, Name, Version, Location (string legible), Enabled y FromPolicy para cada extensión. Si userDataDir es vacío usa ~/.config/chromium. Cuando falta el campo manifest en Preferences intenta leer el manifest.json desde el disco (Extensions/<id>/<ver>/manifest.json)."
tags: [chrome, chromium, browser, profile, extensions, navegator]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["encoding/json", "fmt", "os", "path/filepath", "sort"]
params:
- name: userDataDir
desc: "Ruta al user-data-dir de Chrome/Chromium (ej. ~/.config/chromium, ~/.config/google-chrome). Vacío = ~/.config/chromium."
- name: profileDir
desc: "Nombre del subdirectorio del perfil dentro de userDataDir (ej. 'Default', 'Profile 1'). Debe coincidir con el valor de --profile-directory del proceso Chrome."
output: "Slice de ProfileExtension ordenado por ID (orden determinista). Error si Preferences no existe o contiene JSON inválido. Slice vacío sin error si el perfil no tiene ninguna extensión registrada."
tested: true
tests:
- "dos extensiones con IDs ordenados y campos correctos"
- "extension con state 0 tiene Enabled false"
- "perfil sin Preferences devuelve error"
- "Preferences sin extensions.settings devuelve slice vacío sin error"
- "fallback a Extensions/<id>/<ver>/manifest.json cuando falta manifest en Preferences"
- "location 5 y 10 también son FromPolicy true"
- "Preferences con JSON inválido devuelve error"
test_file_path: "functions/browser/list_chrome_profile_extensions_test.go"
file_path: "functions/browser/list_chrome_profile_extensions.go"
---
## Ejemplo
```go
// Listar extensiones del perfil Default del Chromium del usuario
exts, err := browser.ListChromeProfileExtensions("", "Default")
if err != nil {
log.Fatal(err)
}
for _, e := range exts {
policy := ""
if e.FromPolicy {
policy = " [policy]"
}
enabled := "off"
if e.Enabled {
enabled = "on"
}
fmt.Printf("%s %-40s v%-12s %-24s %s%s\n",
e.ID, e.Name, e.Version, e.Location, enabled, policy)
}
// Output (ejemplo):
// dddbmnkl uBlock Origin Lite v1.0.2 external_policy_download on [policy]
// hklob123 My Dev Extension v0.1.0 unpacked on
// Con ruta explícita (Google Chrome)
exts, err = browser.ListChromeProfileExtensions("/home/user/.config/google-chrome", "Default")
```
## Cuando usarla
Antes de automatizar un perfil de Chrome/Chromium con CDP para auditar qué extensiones están activas, detectar extensiones instaladas por política (FromPolicy) o verificar que una extensión concreta está habilitada. También útil para depurar comportamientos inesperados del navegador causados por extensiones desconocidas.
## Gotchas
- **Preferences puede estar bloqueado mientras Chrome está abierto.** En la práctica Chrome escribe atómicamente el archivo y el bloqueo es brevísimo, pero si el proceso está escribiendo en ese instante `os.ReadFile` puede devolver datos parciales. Usar cuando Chrome no esté activo o tolerar reintento.
- **manifest.name puede ser una clave i18n** (`__MSG_appName__`). En ese caso el `Name` devuelto será esa clave, no el string localizado. Las extensiones empaquetadas en el repositorio de Chrome suelen tener el nombre resuelto directamente en el JSON, pero las extensiones no publicadas pueden usar i18n.
- **Extensions del sistema (location 5 = component) siempre tienen FromPolicy = true** aunque no vengan de una política de empresa; son extensiones internas del propio Chromium (PDF viewer, etc.).
- **Extensiones desinstaladas con estado de caché** pueden aparecer en `extensions.settings` con `state: 0` pero sin directorio en `Extensions/`. Esto es normal; `ListChromeProfileExtensions` las devuelve con `Enabled: false`.
- **Profile Directory ≠ Profile Name.** El parámetro `profileDir` debe ser el nombre del directorio (ej. `"Profile 1"`), que corresponde al `Dir` de `ChromeProfile` devuelto por `list_chrome_profiles_go_browser`.
- En Chrome (Google) el user-data-dir por defecto suele ser `~/.config/google-chrome`; en Chromium `~/.config/chromium`. Pasar ruta explícita si no usas Chromium.
@@ -0,0 +1,252 @@
package browser
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// writePreferences writes a synthetic Preferences JSON file into profileDir.
func writePreferences(t *testing.T, profileDir string, settings map[string]any) {
t.Helper()
prefs := map[string]any{
"extensions": map[string]any{
"settings": settings,
},
}
data, err := json.Marshal(prefs)
if err != nil {
t.Fatalf("json.Marshal Preferences: %v", err)
}
if err := os.WriteFile(filepath.Join(profileDir, "Preferences"), data, 0o600); err != nil {
t.Fatalf("WriteFile Preferences: %v", err)
}
}
func TestListChromeProfileExtensions(t *testing.T) {
t.Run("dos extensiones con IDs ordenados y campos correctos", func(t *testing.T) {
tmpDir := t.TempDir()
profilePath := filepath.Join(tmpDir, "Default")
os.MkdirAll(profilePath, 0o755)
settings := map[string]any{
// location 7 = external_policy_download, state 1 = enabled
"dddbmnkl": map[string]any{
"manifest": map[string]any{
"name": "uBlock Origin Lite",
"version": "1.0.2",
},
"location": 7,
"state": 1,
},
// location 4 = unpacked, state 1 = enabled
"aaabcdef": map[string]any{
"manifest": map[string]any{
"name": "My Dev Extension",
"version": "0.1.0",
},
"location": 4,
"state": 1,
},
}
writePreferences(t, profilePath, settings)
exts, err := ListChromeProfileExtensions(tmpDir, "Default")
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if len(exts) != 2 {
t.Fatalf("esperaba 2 extensiones, got %d", len(exts))
}
// Sorted by ID: "aaabcdef" < "dddbmnkl"
if exts[0].ID != "aaabcdef" {
t.Errorf("exts[0].ID = %q, want %q", exts[0].ID, "aaabcdef")
}
if exts[1].ID != "dddbmnkl" {
t.Errorf("exts[1].ID = %q, want %q", exts[1].ID, "dddbmnkl")
}
// Check names and versions
if exts[0].Name != "My Dev Extension" {
t.Errorf("exts[0].Name = %q, want %q", exts[0].Name, "My Dev Extension")
}
if exts[1].Name != "uBlock Origin Lite" {
t.Errorf("exts[1].Name = %q, want %q", exts[1].Name, "uBlock Origin Lite")
}
if exts[0].Version != "0.1.0" {
t.Errorf("exts[0].Version = %q, want %q", exts[0].Version, "0.1.0")
}
if exts[1].Version != "1.0.2" {
t.Errorf("exts[1].Version = %q, want %q", exts[1].Version, "1.0.2")
}
// FromPolicy: location 7 → true; location 4 → false
if exts[0].FromPolicy {
t.Errorf("exts[0] (unpacked): FromPolicy debe ser false")
}
if !exts[1].FromPolicy {
t.Errorf("exts[1] (external_policy_download): FromPolicy debe ser true")
}
// Location strings
if exts[0].Location != "unpacked" {
t.Errorf("exts[0].Location = %q, want %q", exts[0].Location, "unpacked")
}
if exts[1].Location != "external_policy_download" {
t.Errorf("exts[1].Location = %q, want %q", exts[1].Location, "external_policy_download")
}
// Both enabled
if !exts[0].Enabled {
t.Error("exts[0]: Enabled debe ser true")
}
if !exts[1].Enabled {
t.Error("exts[1]: Enabled debe ser true")
}
})
t.Run("extension con state 0 tiene Enabled false", func(t *testing.T) {
tmpDir := t.TempDir()
profilePath := filepath.Join(tmpDir, "Default")
os.MkdirAll(profilePath, 0o755)
settings := map[string]any{
"extdisabled": map[string]any{
"manifest": map[string]any{
"name": "Disabled Ext",
"version": "2.0.0",
},
"location": 1,
"state": 0,
},
}
writePreferences(t, profilePath, settings)
exts, err := ListChromeProfileExtensions(tmpDir, "Default")
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if len(exts) != 1 {
t.Fatalf("esperaba 1 extensión, got %d", len(exts))
}
if exts[0].Enabled {
t.Error("Enabled debe ser false cuando state=0")
}
})
t.Run("perfil sin Preferences devuelve error", func(t *testing.T) {
tmpDir := t.TempDir()
os.MkdirAll(filepath.Join(tmpDir, "Default"), 0o755)
// No Preferences file created.
_, err := ListChromeProfileExtensions(tmpDir, "Default")
if err == nil {
t.Error("esperaba error al faltar Preferences, got nil")
}
})
t.Run("Preferences sin extensions.settings devuelve slice vacío sin error", func(t *testing.T) {
tmpDir := t.TempDir()
profilePath := filepath.Join(tmpDir, "Default")
os.MkdirAll(profilePath, 0o755)
// Write a Preferences with no extensions key at all.
if err := os.WriteFile(filepath.Join(profilePath, "Preferences"), []byte(`{}`), 0o600); err != nil {
t.Fatal(err)
}
exts, err := ListChromeProfileExtensions(tmpDir, "Default")
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if len(exts) != 0 {
t.Errorf("esperaba slice vacío, got %d elementos", len(exts))
}
})
t.Run("fallback a Extensions/<id>/<ver>/manifest.json cuando falta manifest en Preferences", func(t *testing.T) {
tmpDir := t.TempDir()
profilePath := filepath.Join(tmpDir, "Default")
os.MkdirAll(profilePath, 0o755)
const extID = "fallbackext"
const extVer = "3.1.0"
// Preferences entry without a manifest field.
settings := map[string]any{
extID: map[string]any{
"location": 4,
"state": 1,
// no "manifest" key
},
}
writePreferences(t, profilePath, settings)
// Create the on-disk manifest.json.
manifestDir := filepath.Join(profilePath, "Extensions", extID, extVer)
os.MkdirAll(manifestDir, 0o755)
manifestData, _ := json.Marshal(map[string]any{
"name": "Fallback Extension",
"version": extVer,
})
os.WriteFile(filepath.Join(manifestDir, "manifest.json"), manifestData, 0o600)
exts, err := ListChromeProfileExtensions(tmpDir, "Default")
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if len(exts) != 1 {
t.Fatalf("esperaba 1 extensión, got %d", len(exts))
}
if exts[0].Name != "Fallback Extension" {
t.Errorf("Name = %q, want %q", exts[0].Name, "Fallback Extension")
}
if exts[0].Version != extVer {
t.Errorf("Version = %q, want %q", exts[0].Version, extVer)
}
})
t.Run("location 5 y 10 también son FromPolicy true", func(t *testing.T) {
tmpDir := t.TempDir()
profilePath := filepath.Join(tmpDir, "Default")
os.MkdirAll(profilePath, 0o755)
settings := map[string]any{
"comp0000": map[string]any{
"manifest": map[string]any{"name": "Component Ext", "version": "1.0"},
"location": 5,
"state": 1,
},
"poli0000": map[string]any{
"manifest": map[string]any{"name": "Policy Ext", "version": "1.0"},
"location": 10,
"state": 1,
},
}
writePreferences(t, profilePath, settings)
exts, err := ListChromeProfileExtensions(tmpDir, "Default")
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
for _, ext := range exts {
if !ext.FromPolicy {
t.Errorf("extensión %q (location component/policy) debe tener FromPolicy=true", ext.ID)
}
}
})
t.Run("Preferences con JSON inválido devuelve error", func(t *testing.T) {
tmpDir := t.TempDir()
profilePath := filepath.Join(tmpDir, "Default")
os.MkdirAll(profilePath, 0o755)
os.WriteFile(filepath.Join(profilePath, "Preferences"), []byte(`{invalid json`), 0o600)
_, err := ListChromeProfileExtensions(tmpDir, "Default")
if err == nil {
t.Error("esperaba error con JSON inválido, got nil")
}
})
}