diff --git a/CLAUDE.md b/CLAUDE.md index 8958c95..fdfe628 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,10 +32,11 @@ dotfiles/ │ ├── git-helpers.zsh # git-workdev, git-personaldev, git-whoami, clone-* │ ├── pcsoft-helpers.zsh # is-pcsoft-forbidden, is-pcsoft-editable │ ├── screenshots.zsh # ss, last, ssd, imgclip -│ └── tmux-helpers.zsh # ta, tn, tk, tl, tdev +│ ├── ssh-helpers.zsh # ssh/mosh con contexto visible +│ └── herdr-helpers.zsh # hhere, hremote, hname, whereami ├── starship/starship.toml # Prompt Gruvbox Dark → symlink a ~/.config/starship.toml ├── ghostty/config # Terminal → symlink a ~/.config/ghostty/config -├── tmux/tmux.conf # Multiplexor → symlink a ~/.tmux.conf +├── herdr/config.toml # Multiplexor → symlink a ~/.config/herdr/config.toml ├── local/ │ └── env.zsh.example # Template para local/env.zsh (gitignored) └── install.sh # Instalador diff --git a/README.md b/README.md index 543c1a7..8b47f27 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de A ## Stack -- **Terminal**: [Ghostty](https://ghostty.org/) y Alacritty +- **Terminal**: [Ghostty](https://ghostty.org/) - **Shell**: Zsh nativo de macOS - **Prompt**: [Starship](https://starship.rs/) — tema Gruvbox Dark -- **Multiplexor**: tmux + helpers de sesión +- **Multiplexor**: [Herdr](https://herdr.dev/) para sesiones locales/remotas persistentes - **Barra macOS**: [SketchyBar](https://github.com/FelixKratz/SketchyBar) con tema Gruvbox - **Window manager macOS**: [yabai](https://github.com/koekeishiya/yabai) + [skhd](https://github.com/koekeishiya/skhd) opcional y gradual - **Keyboard remaps macOS**: [Karabiner-Elements](https://karabiner-elements.pqrs.org/) con profile `cortex` - **Editor terminal**: [micro](https://micro-editor.github.io/) - **Editor principal**: Neovim basado en LazyVim/Gentleman.Dots con overlay RefactorIA - **Ls**: [eza](https://github.com/eza-community/eza) -- **AI CLI UX**: Claude Code statusline, OpenCode helpers y cmux hooks opcionales +- **AI CLI UX**: Claude Code statusline, OpenCode helpers y sesiones Herdr por repo - **Supply-chain guardrails**: defaults globales para `uv`, `npm`, `pnpm` y `bun` - **Fuente**: FiraCode Nerd Font + variante custom RefactorIA @@ -29,7 +29,7 @@ bash install.sh ``` El instalador macOS: -1. Instala dependencias via Homebrew (starship, tmux, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) +1. Instala dependencias via Homebrew (starship, herdr, mosh, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) 2. Hace backup de configs existentes con timestamp 3. Crea symlinks de los dotfiles y guardrails globales (`.npmrc`, `pnpm/rc`, `.bunfig.toml`, `uv.toml`) 4. Intenta seleccionar el profile `cortex` de Karabiner si `karabiner_cli` está disponible @@ -41,8 +41,9 @@ El instalador macOS: ``` dotfiles/ ├── claude/ # Claude Code statusline -├── ghostty/ # Config Ghostty, cmux Ghostty config, muxy legado y shaders +├── ghostty/ # Config Ghostty y shaders ├── fonts/ # Fuente RefactorIA y script de regeneración +├── herdr/ # Config Herdr ├── npm/ # Global npm defaults (~/.npmrc) ├── pnpm/ # Global pnpm defaults (~/Library/Preferences/pnpm/rc) ├── bun/ # Global bun defaults (~/.bunfig.toml) @@ -52,11 +53,11 @@ dotfiles/ │ └── scripts/ │ ├── claude-helpers.zsh # Integración Claude Code │ ├── git-helpers.zsh # Identidades Git y clone helpers -│ ├── tmux-helpers.zsh # Helpers tmux +│ ├── ssh-helpers.zsh # SSH/Mosh con contexto visible +│ ├── herdr-helpers.zsh # Helpers Herdr para sesiones y orientación │ ├── worktree-helpers.zsh # Helpers git worktree │ ├── screenshots.zsh # Manejo de screenshots macOS │ └── pcsoft-helpers.zsh # Protección archivos PCSoft -├── tmux/ # Config tmux ├── lazygit/ # Config lazygit ├── karabiner/ # Config Karabiner-Elements (~/.config/karabiner/karabiner.json) ├── micro/ # Settings y themes de micro @@ -81,8 +82,18 @@ dotfiles/ | `cortex`, `dotfiles` | Navegación rápida al repo `cortex` y sus dotfiles | | `cc [path]` | Abrir Claude Code | | `oc [path]` | Abrir OpenCode | +| `hhere`, `hmain` | Volver a la sesión Herdr principal del repo/branch | +| `hnew [path]` | Crear sesión Herdr independiente con timestamp | +| `hrole [path]` | Entrar/crear sesión Herdr por rol operativo | +| `hfocus`, `hside`, `hscratch` | Sesiones Herdr por rol para foco, lateral o scratch | +| `hremote [session]` | Attach remoto con `herdr --remote` | +| `hname [label]` | Nombrar el pane Herdr actual | +| `moshx [remote-path]` | Mosh al host; con path entra a ese directorio remoto | +| `moshx-doctor ` | Verifica `mosh-server`, `herdr`, `git` y `sh` en el host remoto | +| `sshx ` | SSH directo con contexto visible | +| `sshc ` | SSH directo con host visible en prompt | +| `whereami` | Mostrar host, cwd, repo, sesión y SSH | | `ccclip ` | Copiar código al clipboard | -| `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers tmux (`tcc` abre Claude Code en tmux) | | `wtadd`, `wtlist`, `wtremove` | Helpers de git worktrees | | `ss [n]` | Listar últimos screenshots | | `last [-c\|-o]` | Último screenshot | @@ -98,9 +109,40 @@ Editá `local/env.zsh` (gitignored) para configurar: - `SCREENSHOTS_DIR` — directorio de screenshots - `WORKSPACE_DIR` — directorio raíz de tus proyectos - `OPENCODE_DEFAULT_FLAGS` — flags por defecto para `oc` +- `CORTEX_MULTIPLEXER=herdr` — marca Herdr como multiplexor operativo para prompt/helpers - `INNIT_DIR` y overrides `INNIT_*_DIR` — navegación rápida de subdirectorios - Aliases y paths personales +## Herdr remoto + +Para SSH/remoto, el modelo recomendado es Herdr. Usá sesiones nombradas para separar tableros persistentes, workspaces por repo, tabs por objetivo y panes por agente/proceso: + +```bash +hremote agent-dev-01 main +hhere ~/dev/personal/cortex +hfocus ~/dev/personal/cortex +hside ~/dev/personal/cortex +``` + +Si necesitás diagnosticar dependencias: + +```bash +moshx-doctor agent-dev-01 +``` + +Comportamiento: + +- `hhere [path]` y `hmain [path]` nombran la sesión principal por host + repo + branch, para reattach exacto. +- `hnew [path]` crea otra sesión independiente del mismo repo/branch con timestamp corto. +- `hrole [path]`, `hfocus [path]`, `hside [path]` y `hscratch [path]` crean/entran a sesiones independientes por intención operativa, no por monitor físico. +- Si ya estás dentro de Herdr, esos helpers no intentan abrir Herdr anidado: crean o enfocan un workspace con el mismo nombre dentro de la sesión actual. +- `hremote [session]` usa el bridge remoto de Herdr. +- `hname [label]` evita panes anónimos en el sidepanel. +- `cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` ejecutan el agente en el pane actual; Herdr provee persistencia. +- Herdr también expone CLI scriptable para `workspace`, `tab`, `pane`, `agent`, `worktree`, `wait` e `integration`; los helpers solo cubren el flujo muscular diario. +- Prompt Starship marca `herdr` cuando `CORTEX_MULTIPLEXER=herdr`. +- `whereami` muestra ubicación completa sin depender de la UI. + ## SketchyBar La config macOS enlaza `sketchybar/` en `~/.config/sketchybar`. El diseño es sobrio, notch-safe y usa la paleta dark/green de InnIT. @@ -110,9 +152,9 @@ Layout activo: | Pantalla | Uso | Layout | |------|-----|--------| | Mac Retina (`display=1`) | apps generales: Discord, WhatsApp, Mail, Postman, Zen Browser | app activa + network, volumen, calendario, hora, batería | -| ViewSonic vertical (`display=2`) | auxiliar/random, cmux y Claude de formato vertical | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | -| LG Ultrawide (`display=3`) | mixto: cmux, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | -| 4K derecho (`display=4`) | cmux exclusivo | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| ViewSonic vertical (`display=2`) | auxiliar/random, Ghostty/Herdr y Claude de formato vertical | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| LG Ultrawide (`display=3`) | mixto: Ghostty/Herdr, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| 4K derecho (`display=4`) | Ghostty/Herdr exclusivo | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | El centro queda libre para evitar el notch y reducir ruido visual. @@ -126,7 +168,7 @@ Interacciones: | Batería | abre Battery Settings | | Fecha/hora | abre Calendar | -La barra asume Mac con notch y varios monitores: el centro queda libre y los items operativos se mantienen en los laterales. Los items de contexto (`git`, issue/PR, SDD y timer) se actualizan con un agregador liviano que lee `${XDG_CACHE_HOME:-~/.cache}/cortex/active-workspace`. Un watcher de eventos `workspace.selected` de cmux actualiza ese archivo y refresca los items al cambiar de workspace. `SKETCHYBAR_WORKSPACE` permite forzar un repo específico. +La barra asume Mac con notch y varios monitores: el centro queda libre y los items operativos se mantienen en los laterales. Los items de contexto (`git`, issue/PR, SDD y timer) se actualizan con un agregador liviano que lee `${XDG_CACHE_HOME:-~/.cache}/cortex/active-workspace`. `SKETCHYBAR_WORKSPACE` permite forzar un repo específico. El layout cambia automáticamente al recargar SketchyBar: con un solo display, el Retina mantiene los indicadores de estado útiles; con varios displays, el Retina queda liviano y el layout completo se mueve al externo disponible. @@ -251,7 +293,7 @@ El prompt usa una variante local de FiraCode Nerd Font Mono con el glyph de la b | Codepoint | `U+F0F00` | | Glyph test | `python3 -c 'print("\U000F0F00")'` | -La config de Starship usa este glyph PUA directamente. Si la terminal no tiene seleccionada `FiraCode Nerd Font Mono Beard`, el prompt puede mostrar un cuadrado/tofu en lugar de la barba. En macOS, `install.sh` instala la fuente y deja configurados Ghostty y cmux con esa family. +La config de Starship usa este glyph PUA directamente. Si la terminal no tiene seleccionada `FiraCode Nerd Font Mono Beard`, el prompt puede mostrar un cuadrado/tofu en lugar de la barba. En macOS, `install.sh` instala la fuente y deja configurado Ghostty con esa family. Para regenerar la fuente: diff --git a/TODO.md b/TODO.md index 1522e30..d21775e 100644 --- a/TODO.md +++ b/TODO.md @@ -8,8 +8,6 @@ Mejoras identificadas en dotfiles externos de referencia. - [x] **Glyph custom RefactorIA para Nerd Font** — `FiraCode Nerd Font Mono Beard` versionado en `fonts/FiraCodeNerdFontMonoBeard-Reg.ttf`, instalado en `~/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf`, codepoint `U+F0F00`, script regenerable en `fonts/patch_beard.py`, Ghostty y Starship configurados. - [x] **Statusline custom para Claude Code** — script que muestra barra visual de uso del contexto (verde/amarillo/rojo), modelo activo, rama git y porcentaje exacto. -- [x] **Sesión flotante Alt+G en tmux** — popup flotante sobre cualquier layout. -- [x] **tmux-resurrect** — guarda y restaura sesiones tmux al reiniciar. `Prefix+Ctrl+S` / `Prefix+Ctrl+R`. ## Media prioridad @@ -23,7 +21,6 @@ Mejoras identificadas en dotfiles externos de referencia. ## Baja prioridad - [x] **Shaders de cursor en Ghostty** — 4 shaders disponibles en `ghostty/shaders/`. Activo: cursor_smear_gentleman. Para cambiar: editar `custom-shader` en ghostty/config. -- [x] **`tmux-which-key`** — muestra keybindings al presionar el prefix. - [x] **`window-padding-balance = true` en Ghostty** — padding balanceado en splits. --- @@ -44,6 +41,6 @@ Mejoras identificadas en dotfiles externos de referencia. ## Worktrees — integración natural con Claude -- [x] **Worktree helpers zsh + cmux** — `wtadd`/`wtlist`/`wtremove` en `worktree-helpers.zsh`. Mecánica pura: crear directorio hermano, detectar repos PCSoft via `is-pcsoft-forbidden`, abrir workspace cmux automáticamente. Los comandos son la infraestructura, no el punto de entrada al usuario. +- [x] **Worktree helpers zsh + Herdr** — `wtadd`/`wtlist`/`wtremove` en `worktree-helpers.zsh`. Mecánica pura: crear directorio hermano, detectar repos PCSoft via `is-pcsoft-forbidden`, abrir agente en el flujo Herdr. Los comandos son la infraestructura, no el punto de entrada al usuario. -- [x] **Regla de evaluación proactiva en claude-config** — Claude evalúa si worktrees convienen y los sugiere/implementa sin que el usuario lo pida. Triggers: "hay un bug urgente y estoy en medio de algo", feature branch larga + hotfix simultáneo, "no quiero perder contexto pero necesito cambiar de rama". Si el repo es PCSoft → nunca sugerir. Si es no-PCSoft y hay conflicto de contexto → proponer `wtadd` + nuevo workspace cmux directamente. +- [x] **Regla de evaluación proactiva en claude-config** — Claude evalúa si worktrees convienen y los sugiere/implementa sin que el usuario lo pida. Triggers: "hay un bug urgente y estoy en medio de algo", feature branch larga + hotfix simultáneo, "no quiero perder contexto pero necesito cambiar de rama". Si el repo es PCSoft → nunca sugerir. Si es no-PCSoft y hay conflicto de contexto → proponer `wtadd` + nuevo workspace Herdr directamente. diff --git a/ghostty/cmux.conf b/ghostty/cmux.conf index 440013b..1482af1 100644 --- a/ghostty/cmux.conf +++ b/ghostty/cmux.conf @@ -32,27 +32,27 @@ palette = 15=#ffffff # Transparencia background-opacity = 1.0 -unfocused-split-opacity = 0.2 +unfocused-split-opacity = 0.85 # Ventana -window-decoration = false +window-decoration = true window-padding-balance = true window-step-resize = false -macos-titlebar-style = hidden -window-padding-x = 8 -window-padding-y = 8 +macos-titlebar-style = tabs +window-padding-x = 4 +window-padding-y = 2 # Cursor cursor-style = block_hollow -# Cursor con efecto suave -# Opciones disponibles: cursor_smear_soft, cursor_blaze, cursor_frozen, cursor_smear_rainbow -custom-shader = shaders/cursor_smear_soft.glsl +# Shaders desactivados en modo trabajo remoto: reducen legibilidad en multiplexers. +# custom-shader = shaders/cursor_smear_soft.glsl # Integración con shell shell-integration = zsh -# Sin macos-option-as-alt: Option sigue siendo Option en ambos lados (tildes/ñ funcionan normal) +# Option izquierdo como Alt para TUIs; Option derecho conserva tildes/ñ. +macos-option-as-alt = left # Comportamiento scrollback-limit = 10000 diff --git a/ghostty/config b/ghostty/config index 440013b..90be0bd 100644 --- a/ghostty/config +++ b/ghostty/config @@ -1,5 +1,6 @@ -# Fuente +# Fuente primaria: FiraCode Beard custom. Fallback: Nerd Font oficial para glyphs. font-family = FiraCode Nerd Font Mono Beard +font-family = FiraCode Nerd Font font-size = 14 # Paleta Dark Vibrant (portada desde Windows Terminal) @@ -32,27 +33,27 @@ palette = 15=#ffffff # Transparencia background-opacity = 1.0 -unfocused-split-opacity = 0.2 +unfocused-split-opacity = 0.85 # Ventana -window-decoration = false +window-decoration = true window-padding-balance = true window-step-resize = false -macos-titlebar-style = hidden -window-padding-x = 8 -window-padding-y = 8 +macos-titlebar-style = tabs +window-padding-x = 4 +window-padding-y = 2 # Cursor cursor-style = block_hollow -# Cursor con efecto suave -# Opciones disponibles: cursor_smear_soft, cursor_blaze, cursor_frozen, cursor_smear_rainbow -custom-shader = shaders/cursor_smear_soft.glsl +# Shaders desactivados en modo trabajo remoto: reducen legibilidad en multiplexers. +# custom-shader = shaders/cursor_smear_soft.glsl # Integración con shell shell-integration = zsh -# Sin macos-option-as-alt: Option sigue siendo Option en ambos lados (tildes/ñ funcionan normal) +# Option izquierdo como Alt para TUIs; Option derecho conserva tildes/ñ. +macos-option-as-alt = left # Comportamiento scrollback-limit = 10000 diff --git a/ghostty/muxy.conf b/ghostty/muxy.conf index f5b1373..e9b4e92 100644 --- a/ghostty/muxy.conf +++ b/ghostty/muxy.conf @@ -5,27 +5,27 @@ font-size = 14 # Transparencia background-opacity = 1.0 -unfocused-split-opacity = 0.2 +unfocused-split-opacity = 0.85 # Ventana -window-decoration = false +window-decoration = true window-padding-balance = true window-step-resize = false -macos-titlebar-style = hidden -window-padding-x = 8 -window-padding-y = 8 +macos-titlebar-style = tabs +window-padding-x = 4 +window-padding-y = 2 # Cursor cursor-style = block_hollow -# Cursor con efecto suave -# Opciones disponibles: cursor_smear_soft, cursor_blaze, cursor_frozen, cursor_smear_rainbow -custom-shader = shaders/cursor_smear_soft.glsl +# Shaders desactivados en modo trabajo remoto: reducen legibilidad en multiplexers. +# custom-shader = shaders/cursor_smear_soft.glsl # Integración con shell shell-integration = zsh -# Sin macos-option-as-alt: Option sigue siendo Option en ambos lados (tildes/ñ funcionan normal) +# Option izquierdo como Alt para TUIs; Option derecho conserva tildes/ñ. +macos-option-as-alt = left # Comportamiento scrollback-limit = 10000 diff --git a/herdr/config.toml b/herdr/config.toml new file mode 100644 index 0000000..6704915 --- /dev/null +++ b/herdr/config.toml @@ -0,0 +1,25 @@ +[ui.toast] +delivery = "terminal" + +[ui] +sidebar_width = 32 +sidebar_min_width = 24 +sidebar_max_width = 42 +agent_panel_sort = "priority" +show_agent_labels_on_pane_borders = true +prompt_new_tab_name = true +pane_borders = true +pane_gaps = true + +[terminal] +new_cwd = "follow" + +[session] +resume_agents_on_restore = true + +[remote] +manage_ssh_config = true + +[theme] +name = "gruvbox" +auto_switch = false diff --git a/install.sh b/install.sh index f291941..c13c56d 100755 --- a/install.sh +++ b/install.sh @@ -1,29 +1,53 @@ #!/bin/bash -# install.sh — Instalador de dotfiles macOS +# install.sh — Instalador de dotfiles macOS/Linux set -e DOTFILES="$(cd "$(dirname "$0")" && pwd)" TIMESTAMP=$(date +%Y%m%d_%H%M%S) +OS="$(uname -s)" +IS_MACOS=false +IS_LINUX=false + +case "$OS" in + Darwin) IS_MACOS=true ;; + Linux) IS_LINUX=true ;; + *) + echo "❌ Sistema no soportado: $OS" + exit 1 + ;; +esac echo "" echo " ╔══════════════════════════════════════╗" -echo " ║ dotfiles — Instalador macOS ║" +echo " ║ dotfiles — Instalador macOS/Linux ║" echo " ╚══════════════════════════════════════╝" echo "" -# --- Verificar dependencias base --- -if ! command -v brew &>/dev/null; then - echo "❌ Homebrew no está instalado. Instalá desde https://brew.sh" - exit 1 -fi +# --- Verificar herramientas --- +echo "📦 Verificando herramientas ($OS)..." -if ! command -v starship &>/dev/null; then - echo "📦 Instalando starship..." - brew install starship -fi +install_with_brew() { + local command_name="$1" + local package_name="$2" + local description="$3" + + if command -v "$command_name" &>/dev/null; then + echo " ✓ $command_name ya instalado" + return + fi -# --- Instalar herramientas opcionales --- -echo "📦 Verificando herramientas..." + if $IS_MACOS; then + if ! command -v brew &>/dev/null; then + echo "❌ Homebrew no está instalado. Instalá desde https://brew.sh" + exit 1 + fi + + echo " → Instalando $description..." + brew install "$package_name" + else + echo " ! $command_name no está instalado; instalalo con el package manager de esta distro si lo necesitás" + fi +} karabiner_app_exists() { [[ -d "/Applications/Karabiner-Elements.app" || -d "$HOME/Applications/Karabiner-Elements.app" ]] @@ -41,80 +65,60 @@ karabiner_cli_path() { fi } -if ! command -v micro &>/dev/null; then - echo " → Instalando micro (editor terminal)..." - brew install micro -else - echo " ✓ micro ya instalado" -fi +install_with_brew starship starship "starship" +install_with_brew micro micro "micro (editor terminal)" +install_with_brew eza eza "eza (ls mejorado)" +install_with_brew herdr herdr "herdr (multiplexor remoto persistente)" +install_with_brew mosh mosh "mosh (SSH resiliente para workstations remotas)" +install_with_brew lazygit lazygit "lazygit (git TUI)" -if ! command -v eza &>/dev/null; then - echo " → Instalando eza (ls mejorado)..." - brew install eza -else - echo " ✓ eza ya instalado" -fi +if $IS_MACOS; then + install_with_brew sketchybar sketchybar "sketchybar (barra macOS)" -if ! command -v tmux &>/dev/null; then - echo " → Instalando tmux (multiplexor de terminal)..." - brew install tmux -else - echo " ✓ tmux ya instalado" -fi - -if ! command -v lazygit &>/dev/null; then - echo " → Instalando lazygit (git TUI)..." - brew install lazygit -else - echo " ✓ lazygit ya instalado" -fi - -if ! command -v sketchybar &>/dev/null; then - echo " → Instalando sketchybar (barra macOS)..." - brew install sketchybar -else - echo " ✓ sketchybar ya instalado" -fi - -if ! command -v yabai &>/dev/null; then - echo " → Instalando yabai (window manager macOS)..." - brew tap koekeishiya/formulae - brew install yabai -else - echo " ✓ yabai ya instalado" -fi + if ! command -v yabai &>/dev/null; then + echo " → Instalando yabai (window manager macOS)..." + brew tap koekeishiya/formulae + brew install yabai + else + echo " ✓ yabai ya instalado" + fi -if ! command -v skhd &>/dev/null; then - echo " → Instalando skhd (hotkeys macOS)..." - brew tap koekeishiya/formulae - brew install skhd -else - echo " ✓ skhd ya instalado" -fi + if ! command -v skhd &>/dev/null; then + echo " → Instalando skhd (hotkeys macOS)..." + brew tap koekeishiya/formulae + brew install skhd + else + echo " ✓ skhd ya instalado" + fi -if ! karabiner_cli_available && ! karabiner_app_exists; then - echo " → Instalando Karabiner-Elements..." - brew install --cask karabiner-elements -else - echo " ✓ Karabiner-Elements ya instalado" + if ! karabiner_cli_available && ! karabiner_app_exists; then + echo " → Instalando Karabiner-Elements..." + brew install --cask karabiner-elements + else + echo " ✓ Karabiner-Elements ya instalado" + fi fi # --- Fuentes --- echo "" echo "📦 Verificando fuentes..." -if ! ls "$HOME/Library/Fonts/FiraCodeNerdFont"* &>/dev/null 2>&1; then - echo " → Instalando FiraCode Nerd Font..." - brew install --cask font-fira-code-nerd-font - echo " ✓ FiraCode Nerd Font instalada" +if $IS_MACOS; then + if ! ls "$HOME/Library/Fonts/FiraCodeNerdFont"* &>/dev/null 2>&1; then + echo " → Instalando FiraCode Nerd Font..." + brew install --cask font-fira-code-nerd-font + echo " ✓ FiraCode Nerd Font instalada" + else + echo " ✓ FiraCode Nerd Font ya instalada" + fi + + mkdir -p "$HOME/Library/Fonts" + cp "$DOTFILES/fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" "$HOME/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" + echo " ✓ FiraCode Nerd Font Mono Beard instalada" else - echo " ✓ FiraCode Nerd Font ya instalada" + echo " - Fuentes macOS omitidas en Linux" fi -mkdir -p "$HOME/Library/Fonts" -cp "$DOTFILES/fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" "$HOME/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" -echo " ✓ FiraCode Nerd Font Mono Beard instalada" - # --- Backup de configs existentes --- echo "" echo "💾 Haciendo backup de configs existentes..." @@ -143,21 +147,26 @@ backup_karabiner_if_exists() { backup_if_exists "$HOME/.zshrc" backup_if_exists "$HOME/.npmrc" -backup_if_exists "$HOME/Library/Preferences/pnpm/rc" backup_if_exists "$HOME/.bunfig.toml" backup_if_exists "$HOME/.config/uv/uv.toml" backup_if_exists "$HOME/.config/starship.toml" -backup_if_exists "$HOME/.config/ghostty/config" -backup_if_exists "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" -backup_if_exists "$HOME/.tmux.conf" +backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.claude/statusline.sh" backup_if_exists "$HOME/.config/lazygit/config.yml" -backup_if_exists "$HOME/.config/sketchybar" -backup_if_exists "$HOME/.config/yabai/yabairc" -backup_if_exists "$HOME/.yabairc" -backup_if_exists "$HOME/.config/skhd/skhdrc" -backup_if_exists "$HOME/.skhdrc" -backup_karabiner_if_exists + +if $IS_MACOS; then + backup_if_exists "$HOME/Library/Preferences/pnpm/rc" + backup_if_exists "$HOME/.config/ghostty/config" + backup_if_exists "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" + backup_if_exists "$HOME/.config/sketchybar" + backup_if_exists "$HOME/.config/yabai/yabairc" + backup_if_exists "$HOME/.yabairc" + backup_if_exists "$HOME/.config/skhd/skhdrc" + backup_if_exists "$HOME/.skhdrc" + backup_karabiner_if_exists +else + backup_if_exists "$HOME/.config/pnpm/rc" +fi # --- Crear symlinks --- echo "" @@ -175,87 +184,97 @@ create_symlink() { create_symlink "$DOTFILES/zsh/zshrc" "$HOME/.zshrc" create_symlink "$DOTFILES/npm/npmrc" "$HOME/.npmrc" -create_symlink "$DOTFILES/pnpm/rc" "$HOME/Library/Preferences/pnpm/rc" create_symlink "$DOTFILES/bun/bunfig.toml" "$HOME/.bunfig.toml" create_symlink "$DOTFILES/uv/uv.toml" "$HOME/.config/uv/uv.toml" create_symlink "$DOTFILES/starship/starship.toml" "$HOME/.config/starship.toml" -create_symlink "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" -create_symlink "$DOTFILES/ghostty/cmux.conf" "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" -create_symlink "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" -create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" +create_symlink "$DOTFILES/herdr/config.toml" "$HOME/.config/herdr/config.toml" chmod +x "$DOTFILES/claude/statusline.sh" create_symlink "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" create_symlink "$DOTFILES/micro/settings.json" "$HOME/.config/micro/settings.json" create_symlink "$DOTFILES/lazygit/config.yml" "$HOME/.config/lazygit/config.yml" -chmod +x "$DOTFILES/sketchybar/sketchybarrc" "$DOTFILES/sketchybar/plugins"/*.sh -create_symlink "$DOTFILES/sketchybar" "$HOME/.config/sketchybar" -chmod +x "$DOTFILES/yabai/yabairc" -create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.config/yabai/yabairc" -create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.yabairc" -create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.config/skhd/skhdrc" -create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.skhdrc" -create_symlink "$DOTFILES/karabiner/karabiner.json" "$HOME/.config/karabiner/karabiner.json" - -KARABINER_CLI="$(karabiner_cli_path || true)" -if [[ -n "$KARABINER_CLI" ]]; then - if "$KARABINER_CLI" --select-profile cortex &>/dev/null; then - echo " ✓ Perfil Karabiner cortex seleccionado" - else - echo " ! No se pudo seleccionar el perfil Karabiner cortex; abrí Karabiner-Elements para activarlo" + +if $IS_MACOS; then + create_symlink "$DOTFILES/pnpm/rc" "$HOME/Library/Preferences/pnpm/rc" + create_symlink "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" + create_symlink "$DOTFILES/ghostty/cmux.conf" "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" + create_symlink "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" + chmod +x "$DOTFILES/sketchybar/sketchybarrc" "$DOTFILES/sketchybar/plugins"/*.sh + create_symlink "$DOTFILES/sketchybar" "$HOME/.config/sketchybar" + chmod +x "$DOTFILES/yabai/yabairc" + create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.config/yabai/yabairc" + create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.yabairc" + create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.config/skhd/skhdrc" + create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.skhdrc" + create_symlink "$DOTFILES/karabiner/karabiner.json" "$HOME/.config/karabiner/karabiner.json" + + KARABINER_CLI="$(karabiner_cli_path || true)" + if [[ -n "$KARABINER_CLI" ]]; then + if "$KARABINER_CLI" --select-profile cortex &>/dev/null; then + echo " ✓ Perfil Karabiner cortex seleccionado" + else + echo " ! No se pudo seleccionar el perfil Karabiner cortex; abrí Karabiner-Elements para activarlo" + fi fi +else + create_symlink "$DOTFILES/pnpm/rc" "$HOME/.config/pnpm/rc" fi -# --- Servicios macOS --- -echo "" -echo "🚀 Asegurando servicios macOS..." - -warn_service() { - local name="$1" - local command_hint="$2" - echo " ! No se pudo iniciar $name. macOS puede requerir permisos en Privacy & Security > Accessibility." - echo " Fallback manual: $command_hint" -} - -if command -v brew &>/dev/null && command -v sketchybar &>/dev/null; then - if brew services start sketchybar &>/dev/null; then - echo " ✓ sketchybar iniciado via brew services" +if $IS_MACOS; then + # --- Servicios macOS --- + echo "" + echo "🚀 Asegurando servicios macOS..." + + warn_service() { + local name="$1" + local command_hint="$2" + echo " ! No se pudo iniciar $name. macOS puede requerir permisos en Privacy & Security > Accessibility." + echo " Fallback manual: $command_hint" + } + + if command -v brew &>/dev/null && command -v sketchybar &>/dev/null; then + if brew services start sketchybar &>/dev/null; then + echo " ✓ sketchybar iniciado via brew services" + else + warn_service "sketchybar" "brew services start sketchybar" + fi else - warn_service "sketchybar" "brew services start sketchybar" + echo " - sketchybar no disponible; se omite" fi -else - echo " - sketchybar no disponible; se omite" -fi -if command -v yabai &>/dev/null; then - if pgrep -x yabai &>/dev/null; then - if yabai --restart-service &>/dev/null; then - echo " ✓ yabai reiniciado" + if command -v yabai &>/dev/null; then + if pgrep -x yabai &>/dev/null; then + if yabai --restart-service &>/dev/null; then + echo " ✓ yabai reiniciado" + else + warn_service "yabai" "yabai --restart-service" + fi + elif yabai --start-service &>/dev/null; then + echo " ✓ yabai iniciado" else - warn_service "yabai" "yabai --restart-service" + warn_service "yabai" "yabai --start-service" fi - elif yabai --start-service &>/dev/null; then - echo " ✓ yabai iniciado" else - warn_service "yabai" "yabai --start-service" + echo " - yabai no disponible; se omite" fi -else - echo " - yabai no disponible; se omite" -fi -if command -v skhd &>/dev/null; then - if pgrep -x skhd &>/dev/null; then - if skhd --reload &>/dev/null; then - echo " ✓ skhd recargado" + if command -v skhd &>/dev/null; then + if pgrep -x skhd &>/dev/null; then + if skhd --reload &>/dev/null; then + echo " ✓ skhd recargado" + else + warn_service "skhd" "skhd --reload" + fi + elif skhd --start-service &>/dev/null; then + echo " ✓ skhd iniciado" else - warn_service "skhd" "skhd --reload" + warn_service "skhd" "skhd --start-service" fi - elif skhd --start-service &>/dev/null; then - echo " ✓ skhd iniciado" else - warn_service "skhd" "skhd --start-service" + echo " - skhd no disponible; se omite" fi else - echo " - skhd no disponible; se omite" + echo "" + echo "🚀 Servicios macOS omitidos en Linux" fi # --- Copiar colorschemes de micro (no pueden ser symlink) --- @@ -279,11 +298,17 @@ echo "" echo "✅ Instalación completada!" echo "" echo " Próximos pasos:" -echo " 1. Abrí una nueva tab en Ghostty para cargar el nuevo profile" +echo " 1. Abrí una nueva shell para cargar el nuevo profile" echo " 2. Editá local/env.zsh con tus paths personales" -echo " 3. Ghostty ya usa FiraCode Nerd Font Mono Beard (reiniciá si no se ve bien)" -echo " 4. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" -echo " 5. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" +if $IS_MACOS; then + echo " 3. Ghostty ya usa FiraCode Nerd Font Mono Beard (reiniciá si no se ve bien)" + echo " 4. Usá hhere, hremote, cc u oc para trabajar dentro de Herdr" + echo " 5. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" + echo " 6. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" +else + echo " 3. Instalá manualmente herramientas faltantes que el script haya marcado con !" + echo " 4. Usá hhere, hremote, cc u oc para trabajar dentro de Herdr" +fi echo "" echo " Para medir el load time:" echo " \$ time zsh -i -c exit" diff --git a/local/env.zsh.example b/local/env.zsh.example index 4334f9c..4652392 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -16,6 +16,9 @@ # OpenCode # export OPENCODE_DEFAULT_FLAGS="" +# Multiplexor preferido para cc/oc y sesiones remotas. +# export CORTEX_MULTIPLEXER="herdr" + # Navegación rápida para proyectos de una organización/equipo # export INNIT_DIR="$WORKSPACE_DIR/innit" # export INNIT_APIS_DIR="$INNIT_DIR/apis" diff --git a/starship/starship.toml b/starship/starship.toml index e975bef..877cbb7 100644 --- a/starship/starship.toml +++ b/starship/starship.toml @@ -1,5 +1,7 @@ # Formato general format = """ +${custom.ssh_context}\ +${custom.herdr_context}\ ${custom.repo}\ [|](fg:color_orange)\ $directory\ @@ -90,6 +92,22 @@ command = "basename \"$(git rev-parse --show-toplevel 2>/dev/null)\" | cut -c1-2 style = "fg:color_fg0 bg:color_orange" format = '[ $output ]($style)' +[custom.ssh_context] +description = "Host visible en shells remotos" +when = "test -n \"$SSH_CONNECTION\"" +shell = ["bash", "--noprofile", "--norc"] +command = "printf 'ssh:%s' \"${CORTEX_SSH_TARGET:-$(hostname -s 2>/dev/null || hostname)}\" | cut -c1-28" +style = "fg:color_fg0 bg:color_red" +format = '[ $output ]($style)[|](fg:color_red)' + +[custom.herdr_context] +description = "Herdr como multiplexor activo" +when = "test \"${CORTEX_MULTIPLEXER:-}\" = herdr" +shell = ["bash", "--noprofile", "--norc"] +command = "printf 'herdr'" +style = "fg:color_fg0 bg:color_purple" +format = '[ $output ]($style)[|](fg:color_purple)' + [custom.sdd_auto] description = "Estado SDD auto del proyecto" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && test -f .ai/sdd-session.json && (jq -r '.execution_mode // empty' .ai/sdd-session.json 2>/dev/null || python3 -c \"import json; print(json.load(open('.ai/sdd-session.json')).get('execution_mode',''))\" 2>/dev/null) | grep -qx 'auto'" diff --git a/tmux/tmux.conf b/tmux/tmux.conf deleted file mode 100644 index e2f568e..0000000 --- a/tmux/tmux.conf +++ /dev/null @@ -1,116 +0,0 @@ -# tmux.conf — Dev Environment -# Prefix: Ctrl+A - -#region Prefix -unbind C-b -set -g prefix C-a -bind C-a send-prefix -#endregion - -#region Terminal -# True color para que los colores del tema funcionen -set -g default-terminal "tmux-256color" -set -ag terminal-overrides ",xterm-256color:RGB" -set -as terminal-features ",*:RGB" -set -as terminal-features ",*:usstyle" -set -as terminal-features ",*:hyperlinks" - -# Propagar extended keys (shift+enter, ctrl+space, etc.) a apps que las soportan -# Requiere terminal exterior con kitty keyboard protocol (Ghostty/muxy/iTerm2/WezTerm/Kitty) -set -g extended-keys on -set -s extended-keys-format csi-u -set -ga terminal-features 'xterm*:extkeys' -#endregion - -#region General -# Numeración desde 1 (más cómodo que 0) -set -g base-index 1 -setw -g pane-base-index 1 -set -g renumber-windows on - -# Sin delay en ESC (bueno para micro/nvim) -set -s escape-time 0 - -# Historia larga -set -g history-limit 50000 - -# Mouse habilitado -set -g mouse on - -# Título de la ventana del terminal -set -g set-titles on -set -g set-titles-string "#S / #W" -#endregion - -#region Vi Keys -# Copy mode con vi keys -setw -g mode-keys vi -bind -T copy-mode-vi v send -X begin-selection -bind -T copy-mode-vi y send -X copy-pipe-and-cancel "pbcopy" -bind -T copy-mode-vi Escape send -X cancel -#endregion - -#region Window/Pane Management -# Splits que mantienen el directorio actual -bind | split-window -h -c "#{pane_current_path}" -bind - split-window -v -c "#{pane_current_path}" -bind c new-window -c "#{pane_current_path}" - -# Navegación entre panes con hjkl -bind h select-pane -L -bind j select-pane -D -bind k select-pane -U -bind l select-pane -R - -# Resize de panes -bind -r H resize-pane -L 5 -bind -r J resize-pane -D 5 -bind -r K resize-pane -U 5 -bind -r L resize-pane -R 5 -#endregion - -#region Reload -bind r source-file ~/.tmux.conf \; display " Config recargada" -#endregion - -#region Scratch Session -# Ventana flotante con Alt+G — sobre cualquier layout, sin romper el contexto actual -bind-key -n M-g if-shell -F '#{==:#{session_name},scratch}' { - detach-client -} { - display-popup -d "#{pane_current_path}" -E "tmux new-session -A -s scratch" -} -#endregion - -#region Plugins -set -g @plugin 'tmux-plugins/tpm' -set -g @plugin 'tmux-plugins/tmux-resurrect' -set -g @plugin 'alexwforsythe/tmux-which-key' -#endregion - -#region Status Bar — Dark -set -g status-style "bg=#0c0c0c,fg=#ffffff" -set -g status-position bottom -set -g status-interval 5 - -# Izquierda: nombre de sesión -set -g status-left "#[fg=#16c60c,bold] #S #[default] " -set -g status-left-length 30 - -# Derecha: hora y fecha -set -g status-right "#[fg=#555555]%H:%M %d/%m " - -# Ventanas -set -g window-status-format "#[fg=#555555] #I:#W " -set -g window-status-current-format "#[fg=#16c60c,bold] #I:#W " - -# Bordes de panes -set -g pane-border-style "fg=#333333" -set -g pane-active-border-style "fg=#16c60c" - -# Mensajes -set -g message-style "bg=#16c60c,fg=#0c0c0c,bold" -#endregion - -# Inicializar TPM (debe ser la última línea del archivo) -run '~/.tmux/plugins/tpm/tpm' diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 0046750..0c0a5c9 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -1,14 +1,5 @@ #region Claude Code Helpers -# Funciones de integración con Claude Code CLI - -_cmux_sidebar_refresh() { - local target="${1:-$PWD}" - local dotfiles_dir="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}" - local script="$dotfiles_dir/zsh/scripts/cmux-sidebar-refresh.sh" - if [[ -x "$script" ]]; then - "$script" "$target" >/dev/null 2>&1 || true - fi -} +# Funciones de integración con Claude Code/OpenCode. Herdr maneja sesiones, panes y persistencia. _workspace_name_for_path() { local target="${1:-$PWD}" @@ -16,8 +7,7 @@ _workspace_name_for_path() { git_root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null || true) if [[ -n "$git_root" ]]; then - local repo_name - local parent_name + local repo_name parent_name repo_name=$(basename "$git_root" | tr '.' '-') parent_name=$(basename "$(dirname "$git_root")" | tr '.' '-') printf '%s-%s' "$parent_name" "$repo_name" @@ -26,253 +16,43 @@ _workspace_name_for_path() { fi } -_cmux_rename_workspace() { - local workspace_id="$1" - local workspace_name="$2" - - if [[ -z "$workspace_id" || -z "$workspace_name" ]]; then - return 0 - fi - - if command -v cmux >/dev/null 2>&1; then - # env -u descarta el socket heredado del proceso padre (cc/oc/ccb/ocb operan sobre la - # intención explícita del usuario — el workspace destino que indicó al lanzar el comando —, - # no sobre el focused actual; por eso solo aplicamos env -u sin --no-caller) - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux rename-workspace --workspace "$workspace_id" "$workspace_name" >/dev/null 2>&1 || true - fi -} - -# Abrir Claude Code en tmux -# Si no está en tmux, crea una sesión nueva -cc() { +_resolve_dir_or_fail() { local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" + resolved=$(cd -q "$target" >/dev/null 2>&1 && pwd) || { + echo "Directorio no encontrado: $target" return 1 - fi - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - # Estamos dentro de cmux - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - # Si el directorio objetivo es el actual, lanzar claude aquí mismo - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - claude --enable-auto-mode --dangerously-skip-permissions - return - fi - - # Directorio diferente: verificar si ya existe un workspace para no duplicar - # env -u descarta el socket heredado: cc opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) + } + printf '%s' "$resolved" +} - if [[ -n "$existing_id" ]]; then - # El workspace ya existe: enfocarlo sin crear uno nuevo - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - # No existe: crear workspace nuevo con claude corriendo - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "claude --enable-auto-mode --dangerously-skip-permissions"; then - echo "⚠️ cmux new-workspace falló, ejecutando claude en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - else - # No estamos en tmux: crear sesión - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && claude --enable-auto-mode --dangerously-skip-permissions" Enter - tmux attach -t "$session" - fi - fi +# Abrir Claude Code en el directorio actual/pasado. +cc() { + local resolved + resolved=$(_resolve_dir_or_fail "${1:-.}") || return 1 + cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions } -# Abrir OpenCode en tmux/cmux -# Mantiene el mismo patrón de uso que cc() pero usando opencode +# Abrir OpenCode en el directorio actual/pasado. oc() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$oc_cmd" - return - fi - - # env -u descarta el socket heredado: oc opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$oc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando OpenCode en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $oc_cmd" Enter - tmux attach -t "$session" - fi - fi + resolved=$(_resolve_dir_or_fail "${1:-.}") || return 1 + cd "$resolved" && opencode ${OPENCODE_DEFAULT_FLAGS:-} } -# Abrir Claude Code con bypass de permisos explícito +# Abrir Claude Code con bypass de permisos explícito. ccb() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local cc_cmd="claude --dangerously-skip-permissions" - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$cc_cmd" - return - fi - - # env -u descarta el socket heredado: ccb opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$cc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando Claude Code en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$cc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$cc_cmd" - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $cc_cmd" Enter - tmux attach -t "$session" - fi - fi + resolved=$(_resolve_dir_or_fail "${1:-.}") || return 1 + cd "$resolved" && claude --dangerously-skip-permissions } -# Abrir OpenCode con bypass de permisos explícito +# Abrir OpenCode con flags por defecto. ocb() { - local target="${1:-.}" - local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$oc_cmd" - return - fi - - # env -u descarta el socket heredado: ocb opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$oc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando OpenCode en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $oc_cmd" Enter - tmux attach -t "$session" - fi - fi + oc "${1:-.}" } -# Abrir Claude Code con contexto inicial en tmux +# Abrir Claude Code con contexto inicial. ccx() { local context="$1" local target="${2:-.}" @@ -283,55 +63,16 @@ ccx() { fi local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - # Estamos dentro de cmux: escribir contexto a tempfile y abrir workspace propio - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - # Archivo de contexto en ~/.claude/ para garantizar accesibilidad desde el workspace nuevo - local ctxfile="$HOME/.claude/ccx-ctx-$$.txt" - echo "$context" > "$ctxfile" - chmod 600 "$ctxfile" - - # Crear workspace nuevo — el cleanup va dentro del comando para que - # el archivo siga existiendo cuando cmux lo lea en el workspace nuevo - # env -u descarta el socket heredado: ccx opera sobre el workspace destino explícito - if env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "sh -c 'claude < $ctxfile; rm -f $ctxfile'"; then - : # limpieza la hace el comando en el workspace nuevo - else - # cmux falló: limpiar archivo y ejecutar claude directo con el contexto - rm -f "$ctxfile" - echo "⚠️ cmux new-workspace falló, ejecutando claude en el directorio actual" - cd "$resolved" && echo "$context" | claude - fi - elif [[ -n "$TMUX" ]]; then - cd "$resolved" && echo "$context" | claude - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && echo ${(q)context} | claude" Enter - tmux attach -t "$session" - fi - fi + resolved=$(_resolve_dir_or_fail "$target") || return 1 + cd "$resolved" && echo "$context" | claude } -# Navegar al Claude workspace +# Navegar al workspace principal. ccd() { local subpath="${1:-}" local workspace="${WORKSPACE_DIR:-$HOME/dev}" - local target + if [[ -n "$subpath" ]]; then target="$workspace/$subpath" else @@ -340,14 +81,14 @@ ccd() { if [[ -d "$target" ]]; then cd "$target" - echo "📂 Navegando a: $target" + echo "Navegando a: $target" else - echo "❌ Directorio no encontrado: $target" + echo "Directorio no encontrado: $target" return 1 fi } -# Copiar contexto de código al clipboard para Claude +# Copiar contexto de código al clipboard para Claude. ccclip() { if [[ $# -eq 0 ]]; then echo "Uso: ccclip [archivo2 ...] [-n|--line-numbers]" @@ -365,32 +106,44 @@ ccclip() { done local context="" + local file ext n line for file in "${files[@]}"; do if [[ ! -f "$file" ]]; then - echo "⚠️ Archivo no encontrado: $file" + echo "Archivo no encontrado: $file" continue fi - local ext="${file##*.}" + ext="${file##*.}" context+="\`\`\`$ext\n" context+="// File: $file\n" if $with_numbers; then - local n=1 + n=1 while IFS= read -r line; do context+=$(printf "%4d: %s\n" "$n" "$line") (( n++ )) done < "$file" else - context+="$(cat "$file")\n" + context+="$(<"$file")\n" fi context+="\`\`\`\n\n" done - echo -e "$context" | pbcopy - echo "✓ Contexto copiado al clipboard (${#files[@]} archivo$([ ${#files[@]} -ne 1 ] && echo 's'))" + if command -v pbcopy >/dev/null 2>&1; then + print -r -- "$context" | pbcopy + elif command -v wl-copy >/dev/null 2>&1; then + print -r -- "$context" | wl-copy + elif command -v xclip >/dev/null 2>&1; then + print -r -- "$context" | xclip -selection clipboard + else + print -r -- "$context" + echo "Clipboard no disponible; imprimí el contexto en stdout" + return 1 + fi + + echo "Contexto copiado al clipboard (${#files[@]} archivo$([ ${#files[@]} -ne 1 ] && echo 's'))" } #endregion diff --git a/zsh/scripts/herdr-helpers.zsh b/zsh/scripts/herdr-helpers.zsh new file mode 100644 index 0000000..9c8a6ba --- /dev/null +++ b/zsh/scripts/herdr-helpers.zsh @@ -0,0 +1,184 @@ +# herdr-helpers.zsh — Remote-first Herdr helpers. + +_herdr_context_label() { + local host + host="${HOST%%.*}" + host="${host:-$(hostname -s 2>/dev/null || hostname)}" + + if [[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + printf 'ssh-%s' "$host" + else + printf 'local-%s' "$host" + fi +} + +_herdr_workspace_name_for_path() { + local target="${1:-$PWD}" + local git_root repo_name parent_name branch + git_root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null || true) + + if [[ -n "$git_root" ]]; then + repo_name=$(basename "$git_root" | tr '.' '-') + parent_name=$(basename "$(dirname "$git_root")" | tr '.' '-') + branch=$(git -C "$git_root" branch --show-current 2>/dev/null || true) + if [[ -n "$branch" ]]; then + printf '%s-%s-%s' "$parent_name" "$repo_name" "${branch//[^A-Za-z0-9_.-]/-}" + else + printf '%s-%s' "$parent_name" "$repo_name" + fi + else + basename "$target" | tr '.' '-' + fi +} + +_herdr_session_name_for_path() { + printf '%s-%s' "$(_herdr_context_label)" "$(_herdr_workspace_name_for_path "${1:-$PWD}")" +} + +_herdr_session_name_with_suffix() { + local session_path="${1:-$PWD}" + local suffix="$2" + + if [[ -n "$suffix" ]]; then + printf '%s-%s' "$(_herdr_session_name_for_path "$session_path")" "${suffix//[^A-Za-z0-9_.-]/-}" + else + _herdr_session_name_for_path "$session_path" + fi +} + +_herdr_open_session_for_path() { + local target="${1:-$PWD}" + local suffix="${2:-}" + local resolved session workspace_label + + resolved=$(cd -q "$target" >/dev/null 2>&1 && pwd) || { + echo "Directorio no encontrado: $target" + return 1 + } + + if [[ "${HERDR_ENV:-}" == "1" ]]; then + workspace_label="$(_herdr_session_name_with_suffix "$resolved" "$suffix")" + workspace_label="${workspace_label#$(_herdr_context_label)-}" + _herdr_focus_or_create_workspace "$workspace_label" "$resolved" + return + fi + + session="$(_herdr_session_name_with_suffix "$resolved" "$suffix")" + CORTEX_MULTIPLEXER=herdr herdr --session "$session" +} + +_herdr_workspace_id_by_label() { + local label="$1" + herdr workspace list 2>/dev/null | python3 -c ' +import json +import sys + +label = sys.argv[1] +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) + +for workspace in data.get("result", {}).get("workspaces", []): + if workspace.get("label") == label: + print(workspace.get("workspace_id", "")) + break +' "$label" 2>/dev/null +} + +_herdr_focus_or_create_workspace() { + local label="$1" + local cwd="$2" + local workspace_id + + workspace_id="$(_herdr_workspace_id_by_label "$label")" + if [[ -n "$workspace_id" ]]; then + herdr workspace focus "$workspace_id" >/dev/null && printf 'workspace: %s\n' "$label" + else + herdr workspace create --cwd "$cwd" --label "$label" --focus >/dev/null && printf 'workspace: %s\n' "$label" + fi +} + +_herdr_current_pane_id() { + command -v herdr >/dev/null 2>&1 || return 1 + herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null +} + +# Entrar/crear una sesión Herdr nombrada por host + repo + branch del path actual. +hhere() { + _herdr_open_session_for_path "${1:-$PWD}" +} + +# Alias semántico de hhere: volver a la sesión principal del repo/branch actual. +hmain() { + hhere "${1:-$PWD}" +} + +# Entrar/crear una sesión Herdr con rol humano. Uso: hrole [path] +hrole() { + local role="${1:?Uso: hrole [path]}" + local target="${2:-$PWD}" + _herdr_open_session_for_path "$target" "$role" +} + +# Entrar/crear una sesión Herdr independiente con timestamp corto. +hnew() { + local target="${1:-$PWD}" + _herdr_open_session_for_path "$target" "new-$(date +%H%M%S)-$RANDOM" +} + +# Entrar/crear la sesión Herdr de trabajo principal/intenso para este repo/branch. +hfocus() { + _herdr_open_session_for_path "${1:-$PWD}" "focus" +} + +# Entrar/crear una sesión Herdr lateral para este repo/branch. +hside() { + _herdr_open_session_for_path "${1:-$PWD}" "side" +} + +# Entrar/crear una sesión Herdr temporal para pruebas o tareas descartables. +hscratch() { + _herdr_open_session_for_path "${1:-$PWD}" "scratch" +} + +# Attach remoto con Herdr. Uso: hremote [session] +hremote() { + local target="${1:?Uso: hremote [session]}" + local session="${2:-main}" + CORTEX_MULTIPLEXER=herdr CORTEX_SSH_TARGET="$target" herdr --remote "$target" --session "$session" +} + +# Renombrar el pane actual de Herdr con un label humano o uno derivado de repo/branch. +hname() { + local label="${1:-$(_herdr_workspace_name_for_path "$PWD")}" + local pane_id + pane_id="$(_herdr_current_pane_id)" || { + echo "No pude detectar el pane actual de Herdr" + return 1 + } + herdr pane rename "$pane_id" "$label" >/dev/null && printf 'pane: %s\n' "$label" +} + +# Mostrar contexto completo del shell/pane actual. +herdr-orient() { + printf 'host: %s\n' "$(hostname -s 2>/dev/null || hostname)" + printf 'cwd: %s\n' "$PWD" + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + printf 'repo: %s\n' "$(git rev-parse --show-toplevel)" + printf 'branch: %s\n' "$(git branch --show-current 2>/dev/null || printf detached)" + fi + [[ -n "$SSH_CONNECTION" ]] && printf 'ssh: %s\n' "${CORTEX_SSH_TARGET:-remote}" + + local pane_id + pane_id="$(_herdr_current_pane_id)" && printf 'herdr: %s\n' "$pane_id" +} + +# Mantener el comando muscular, pero con contexto Herdr incluido cuando existe. +whereami() { + herdr-orient +} + +alias h='herdr' +alias hs='herdr status' +alias hl='herdr workspace list' diff --git a/zsh/scripts/ssh-helpers.zsh b/zsh/scripts/ssh-helpers.zsh new file mode 100644 index 0000000..c7d2dd7 --- /dev/null +++ b/zsh/scripts/ssh-helpers.zsh @@ -0,0 +1,64 @@ +# ssh-helpers.zsh — Remote helpers with visible host/repo context. + +_remote_herdr_command_for_path() { + local remote_path="$1" + local shell_path + + if [[ "$remote_path" == "$HOME/"* ]]; then + shell_path="~/${remote_path#$HOME/}" + else + shell_path="${(q)remote_path}" + fi + + printf 'cd %s && exec ${SHELL:-zsh}' "$shell_path" +} + +# Entrar a una workstation remota por Mosh. Si pasás path, entra directo a ese directorio. +moshx() { + local target="${1:?Uso: moshx [remote-path]}" + shift + + if ! command -v mosh >/dev/null 2>&1; then + echo "mosh no está instalado localmente; fallback: sshx $target ${*}" + sshx "$target" "$@" + return + fi + + local remote_path="${1:-}" + if [[ -z "$remote_path" ]]; then + CORTEX_SSH_TARGET="$target" mosh "$target" + return + fi + shift + + local remote_command + remote_command="$(_remote_herdr_command_for_path "$remote_path")" + CORTEX_SSH_TARGET="$target" mosh "$target" -- sh -lc "$remote_command" || { + echo "mosh falló; probando fallback SSH al mismo directorio remoto" + CORTEX_SSH_TARGET="$target" ssh -t "$target" sh -lc "$remote_command" + } +} + +# Diagnosticar dependencias remotas para moshx/herdr sin abrir sesión interactiva. +moshx-doctor() { + local target="${1:?Uso: moshx-doctor }" + + echo "Local:" + command -v mosh >/dev/null 2>&1 && echo " ✓ mosh: $(command -v mosh)" || echo " ✗ mosh local no encontrado" + command -v herdr >/dev/null 2>&1 && echo " ✓ herdr: $(command -v herdr)" || echo " ✗ herdr local no encontrado" + + echo "Remote $target:" + ssh "$target" 'for cmd in mosh-server herdr git sh; do if command -v "$cmd" >/dev/null 2>&1; then printf " ✓ %s: %s\n" "$cmd" "$(command -v "$cmd")"; else printf " ✗ %s no encontrado\n" "$cmd"; fi; done' +} + +# SSH directo, pero exportando CORTEX_SSH_TARGET para que el prompt muestre el host remoto. +sshc() { + local target="${1:?Uso: sshc [ssh args...]}" + shift + CORTEX_SSH_TARGET="$target" ssh "$target" "$@" +} + +# SSH directo con contexto visible. Herdr se encarga de la persistencia fuera de SSH. +sshx() { + sshc "$@" +} diff --git a/zsh/scripts/tmux-helpers.zsh b/zsh/scripts/tmux-helpers.zsh deleted file mode 100644 index f8ecefc..0000000 --- a/zsh/scripts/tmux-helpers.zsh +++ /dev/null @@ -1,51 +0,0 @@ -# tmux-helpers.zsh — Helpers para gestión de sesiones tmux - -# Alias base -alias t="tmux" - -# Listar sesiones activas -tl() { - tmux ls 2>/dev/null || echo "No hay sesiones tmux activas" -} - -# Attach a una sesión (o crearla si no existe) -ta() { - local session="${1:-main}" - tmux attach -t "$session" 2>/dev/null || tmux new-session -s "$session" -} - -# Nueva sesión con nombre -tn() { - local session="${1:?Uso: tn }" - tmux new-session -s "$session" -} - -# Matar una sesión -tk() { - local session="${1:?Uso: tk }" - tmux kill-session -t "$session" && echo "✓ Sesión '$session' terminada" -} - -# Sesión de desarrollo: nombre = basename del directorio actual -# Uso: cd ~/dev/work/myproject && tdev -tdev() { - local session - session="$(basename "$PWD" | tr '.' '-')" - ta "$session" -} - -# Sesión de Claude Code en tmux -# Uso: cd ~/dev/work/myproject && tcc -tcc() { - local session - session="$(basename "$PWD" | tr '.' '-')" - - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - return - fi - - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "claude" Enter - tmux attach -t "$session" -} diff --git a/zsh/scripts/worktree-helpers.zsh b/zsh/scripts/worktree-helpers.zsh index 47ee32c..6b2610e 100644 --- a/zsh/scripts/worktree-helpers.zsh +++ b/zsh/scripts/worktree-helpers.zsh @@ -1,5 +1,5 @@ #region Worktree Helpers -# Funciones para gestión de git worktrees con integración cmux. +# Funciones para gestión de git worktrees con integración Herdr. # Detecta repos PCSoft automáticamente y bloquea la creación de worktrees en ellos. # Verifica si el repo actual contiene archivos PCSoft (Categoría B — prohibido worktree) @@ -32,7 +32,7 @@ _wt_path_for() { printf '%s/%s/%s' "$(_wt_base_dir)" "$repo_name" "$name" } -# Crea un worktree en ~/dev/worktrees// y abre workspace cmux automáticamente. +# Crea un worktree en ~/dev/worktrees// y abre el agente desde Herdr. # Uso: wtadd [branch] # — nombre del worktree (crea ~/dev/worktrees//) # [branch] — branch existente o nueva (default: crea branch nueva con el mismo nombre) @@ -82,9 +82,9 @@ wtadd() { echo "✓ Worktree creado: $wt_path" - # Abrir workspace cmux si está disponible - if [[ -n "$CMUX_WORKSPACE_ID" ]] || command -v cmux &>/dev/null; then - echo "→ Abriendo workspace cmux..." + # Abrir el agente en el flujo Herdr si está disponible. + if [[ "${CORTEX_MULTIPLEXER:-herdr}" == "herdr" ]]; then + echo "→ Abriendo agente para el worktree..." cc "$wt_path" fi } diff --git a/zsh/zshrc b/zsh/zshrc index 34bfb23..206ee7a 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -56,6 +56,7 @@ export VISUAL="$EDITOR" # Workspace principal de desarrollo export WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/dev}" export CLAUDE_CODE_EFFORT_LEVEL=high +export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-herdr}" # Proyectos de la organización export WORK_PROJECTS_DIR="${WORK_PROJECTS_DIR:-$HOME/dev/work}" @@ -219,7 +220,8 @@ if [[ -d "$_SCRIPTS_DIR" ]]; then [[ -f "$_SCRIPTS_DIR/screenshots.zsh" ]] && source "$_SCRIPTS_DIR/screenshots.zsh" [[ -f "$_SCRIPTS_DIR/claude-helpers.zsh" ]] && source "$_SCRIPTS_DIR/claude-helpers.zsh" [[ -f "$_SCRIPTS_DIR/git-helpers.zsh" ]] && source "$_SCRIPTS_DIR/git-helpers.zsh" - [[ -f "$_SCRIPTS_DIR/tmux-helpers.zsh" ]] && source "$_SCRIPTS_DIR/tmux-helpers.zsh" + [[ -f "$_SCRIPTS_DIR/ssh-helpers.zsh" ]] && source "$_SCRIPTS_DIR/ssh-helpers.zsh" + [[ -f "$_SCRIPTS_DIR/herdr-helpers.zsh" ]] && source "$_SCRIPTS_DIR/herdr-helpers.zsh" [[ -f "$_SCRIPTS_DIR/worktree-helpers.zsh" ]] && source "$_SCRIPTS_DIR/worktree-helpers.zsh" fi #endregion @@ -237,6 +239,37 @@ if command -v atuin &>/dev/null; then fi #endregion +#region Terminal Title +_cortex_terminal_title() { + [[ -t 1 ]] || return 0 + + local host context repo dir title + host="${HOST%%.*}" + host="${host:-$(hostname -s 2>/dev/null || hostname)}" + + if [[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + context="ssh:$host" + else + context="$host" + fi + + repo=$(git rev-parse --show-toplevel 2>/dev/null) + if [[ -n "$repo" ]]; then + dir="$(basename "$repo")" + else + dir="${PWD/#$HOME/~}" + fi + + title="herdr | $context | $dir" + + print -Pn "\e]0;${title}\a" +} + +autoload -Uz add-zsh-hook +add-zsh-hook precmd _cortex_terminal_title +add-zsh-hook chpwd _cortex_terminal_title +#endregion + #region Starship if command -v starship &>/dev/null; then eval "$(starship init zsh)" @@ -245,7 +278,8 @@ fi #region Welcome _welcome() { - local ms=$(( int(($EPOCHREALTIME - _PROFILE_START) * 1000) )) + local ms + ms=$(awk -v start="${_PROFILE_START:-0}" -v now="${EPOCHREALTIME:-0}" 'BEGIN { printf "%d", (now - start) * 1000 }' 2>/dev/null || printf "0") echo "" echo " ╔══════════════════════════════════════╗" @@ -294,29 +328,26 @@ help-profile() { echo " ${C}.., ..., .... ${R}Subir 1, 2 o 3 niveles" echo "\n${T}Claude Code${R}" - echo " ${C}cc [path] ${R}Abrir Claude Code en directorio" - echo " ${C}oc [path] ${R}Abrir OpenCode en directorio" + echo " ${C}cc [path] ${R}Abrir Claude Code en directorio actual/pasado" + echo " ${C}oc [path] ${R}Abrir OpenCode en directorio actual/pasado" echo " ${C}ccx [p] ${R}Claude Code con contexto inicial" echo " ${C}ccd [sub] ${R}Navegar al Claude workspace" echo " ${C}ccclip ${R}Copiar código al clipboard" - echo "\n${T}Tmux — sesiones${R}" - echo " ${C}tcc ${R}Abrir Claude Code + lazygit (layout automático)" - echo " ${C}tdev ${R}Sesión con nombre del directorio actual" - echo " ${C}ta [nombre] ${R}Attach a sesión (o crearla)" - echo " ${C}tn ${R}Nueva sesión con nombre" - echo " ${C}tl ${R}Listar sesiones activas" - echo " ${C}tk ${R}Matar sesión" - - echo "\n${T}Tmux — keybindings${R}" - echo " ${C}Ctrl+A R ${R}Recargar config" - echo " ${C}Ctrl+A Space ${R}Ver todos los keybindings (which-key)" - echo " ${C}Ctrl+A | ${R}Split vertical" - echo " ${C}Ctrl+A - ${R}Split horizontal" - echo " ${C}Ctrl+A hjkl ${R}Navegar entre paneles" - echo " ${C}Ctrl+A Ctrl+S ${R}Guardar sesiones (resurrect)" - echo " ${C}Ctrl+A Ctrl+R ${R}Restaurar sesiones (resurrect)" - echo " ${C}Option+G ${R}Popup flotante (scratch)" + echo "\n${T}SSH / ubicación${R}" + echo " ${C}hhere/hmain [p] ${R}Volver a la sesión Herdr principal del repo/branch" + echo " ${C}hnew [path] ${R}Crear sesión Herdr independiente con timestamp" + echo " ${C}hrole [p] ${R}Entrar/crear sesión Herdr por rol operativo" + echo " ${C}hfocus/hside ${R}Sesiones Herdr por rol: foco o lateral" + echo " ${C}hscratch [path] ${R}Sesión Herdr temporal para pruebas" + echo " ${C}hremote [s] ${R}Attach Herdr remoto a host con sesión opcional" + echo " ${C}hname [label] ${R}Nombrar pane Herdr actual" + echo " ${C}hs, hl ${R}Status Herdr y workspaces" + echo " ${C}moshx [path] ${R}Mosh/SSH resiliente al host; Herdr es el multiplexor" + echo " ${C}moshx-doctor ${R}Verificar mosh-server, herdr, git y sh en el host" + echo " ${C}sshx ${R}SSH directo con contexto visible" + echo " ${C}sshc ${R}SSH directo con host visible en prompt" + echo " ${C}whereami ${R}Mostrar host, cwd, repo, sesión y SSH" echo "" }