diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d0bddc..30bfa50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Install minimal tools run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends zsh jq shellcheck + sudo apt-get install -y --no-install-recommends fish zsh jq shellcheck - name: Validate shell syntax shell: bash @@ -36,6 +36,10 @@ jobs: zsh -n "$file" done + while IFS= read -r -d '' file; do + fish --no-execute "$file" + done < <(find fish -type f -name '*.fish' -print0) + bash_files=( install.sh claude/statusline.sh @@ -52,6 +56,16 @@ jobs: shellcheck --severity=error --shell=bash "${bash_files[@]}" fi + - name: Run Fish migration harnesses + shell: bash + run: | + fish fish/tests/w2-core.fish + fish fish/tests/w3-helpers.fish + fish fish/tests/w4b-screenshots.fish + fish fish/tests/w5a-hremote.fish + fish fish/tests/w5b-herdr-ssh.fish + fish fish/tests/w6-agent-helpers.fish + - name: Validate JSON configs shell: bash run: | @@ -69,6 +83,9 @@ jobs: - name: Check installer symlink safety run: ./scripts/check-install-symlinks.sh + - name: Check pure Nix preflight contract + run: bash scripts/test-nix-preflight.sh + - name: Validate TOML configs shell: bash run: | diff --git a/README.md b/README.md index e8a9d2e..3353f52 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de A ## CI -GitHub Actions ejecuta un smoke check mínimo en pull requests y pushes a `main`: sintaxis Zsh/Bash, JSON con `jq`, TOML con `python3`/`tomllib`, y `./install.sh --check` cuando el instalador lo soporte. +GitHub Actions ejecuta un smoke check mínimo en pull requests y pushes a `main`: sintaxis Zsh/Fish/Bash, el harness aislado de Fish, JSON con `jq`, TOML con `python3`/`tomllib`, y `./install.sh --check` cuando el instalador lo soporte. ## Stack - **Terminal**: [Ghostty](https://ghostty.org/) y Alacritty -- **Shell**: Zsh nativo de macOS +- **Shell**: Zsh nativo de macOS + profile Fish core opt-in (W2) - **Prompt**: [Starship](https://starship.rs/) — tema Gruvbox Dark - **Multiplexor**: tmux + helpers de sesión - **Barra macOS**: [SketchyBar](https://github.com/FelixKratz/SketchyBar) con tema Gruvbox @@ -46,6 +46,7 @@ bash install.sh --dry-run ``` El instalador macOS: + 1. Instala dependencias via Homebrew (cmux, starship, tmux, lazygit, neovim, 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`) @@ -53,6 +54,118 @@ El instalador macOS: 5. Intenta arrancar/recargar `sketchybar`, `yabai` y `skhd` sin cortar la instalación si macOS requiere permisos 6. Crea `local/env.zsh` desde el template +## Límite Nix + Home Manager (W1) + +W1 agrega solamente una base de Flake y Home Manager al repositorio. No instala Nix, no genera `flake.lock`, no activa Home Manager y no cambia ningún archivo del host. + +### Verificación segura ahora + +```bash +bash scripts/nix-preflight.sh +bash scripts/test-nix-preflight.sh +bash -n scripts/nix-preflight.sh scripts/test-nix-preflight.sh +``` + +El preflight es de solo lectura y sin red: usa `--offline` y `--no-write-lock-file`. En una máquina sin Nix termina con estado no cero de forma esperada, pero no cambia el host. Su contrato de readiness exige macOS `arm64` o `x86_64`, un usuario y `HOME` válidos, Nix disponible y los inputs bloqueados ya disponibles localmente. Una configuración Fish existente es una advertencia, no un bloqueo: W1 no la administra. + +### Ownership y próximo bootstrap + +| Área | Owner en W1 | +| --- | --- | +| `/opt/homebrew/bin/fish` | Homebrew actual | +| Symlinks, servicios y fuentes actuales | `install.sh` | +| Zsh y Ghostty | Configuración actual, sin cambios | +| `~/.config/fish/conf.d/99-local.fish` | Host; reservado para secretos/estado privado futuro, no creado ni gestionado por Nix | +| `flake.nix` y `nix/home.nix` | Base inactiva de Home Manager | + +El bootstrap queda explícitamente diferido a un work unit revisado. Después de instalar Nix por fuera de este repositorio, ese trabajo podrá generar el lock con: + +```bash +nix --extra-experimental-features 'nix-command flakes' flake lock +``` + +Ese comando resuelve inputs y modifica `flake.lock`; por eso no es parte de W1. También quedan diferidos cualquier `home-manager switch`, `nix run ... switch`, instalación de paquetes, cambio de shell, `chsh`, servicios o extensiones Fish fuera del core W2. + +La configuración pura de W1 es `homeConfigurations.jbarbat`: declara `username = "jbarbat"`, `homeDirectory = "/Users/jbarbat"` y `system = "aarch64-darwin"` de forma explícita y revisable. Una futura activación debe seleccionar ese target sin derivar valores de la máquina en tiempo de evaluación. + +Para volver atrás de W1 basta quitar `flake.nix`, `nix/`, `scripts/nix-preflight.sh`, `scripts/test-nix-preflight.sh` y esta sección. No hay estado de host que revertir porque W1 no activó nada. + +## Fish core profile (W2) + +W2 agrega un profile Fish inerte y opt-in: no cambia el login shell, no activa Home Manager, no toca `starship.toml` ni crea `99-local.fish`. + +### Uso seguro + +1. Copiá `fish/conf.d/99-local.fish.example` a `~/.config/fish/conf.d/99-local.fish` si necesitás paths o editor locales. +2. Mantené ese archivo fuera de Git: carga después de los defaults rastreados. +3. Probá el profile en una sesión Fish; Zsh permanece sin cambios. + +| Área | Owner en W2 | +| --- | --- | +| `fish/conf.d/10-core.fish` y `fish/functions/*.fish` | Fuentes rastreadas de Fish | +| `~/.config/fish/conf.d/99-local.fish` | Host; override privado no gestionado | +| Home Manager | Mapea cada fuente Fish explícitamente; no gestiona el directorio completo, historial ni variables Fish | +| Starship | Inicialización solo interactiva y cuando el comando existe; su TOML sigue fuera de este profile | + +El core incluye defaults de entorno, selección de editor, aliases Git/navegación y `dev`, `barbat`, `innit` con sus destinos relacionados (`innit-apis`, `innit-mobile`, `innit-webs`, `innit-pcsoft`), además de la navegación simple con `cowork`, `personal`, `tools`, `worktrees` y `work`. La creación, gestión y protecciones de Git worktrees siguen diferidas para W3+, junto con Git/SSH identities, PCSoft, tmux, screenshots, Herdr y helpers de agentes. + +Para validar sin tocar configuración real: + +```bash +/opt/homebrew/bin/fish fish/tests/w2-core.fish +``` + +La evaluación Nix permanece diferida: esta unidad no genera `flake.lock` ni ejecuta activación. + +## Helpers Fish: Git, PCSoft y worktrees (W3) + +W3 suma helpers Fish opt-in para identidades Git, protección de archivos PCSoft y worktrees; no crea `~/.ssh/config`, no clona durante la configuración y no modifica la identidad global de Git. + +### Uso seguro + +1. Copiá `fish/conf.d/99-local.fish.example` a tu `99-local.fish` privado y definí las cuatro variables `GIT_*_NAME` y `GIT_*_EMAIL`. +2. En un repositorio, usá `git-workdev` o `git-personaldev`; si falta un valor privado, el helper falla antes de cambiar la configuración local o el remote. +3. Usá `clone-workdev` o `clone-personaldev` solo cuando quieras clonar: enrutan la URL mediante `github-workdev` o `github-personaldev`. + +| Área | Interfaz rastreada | Estado privado del host | +| --- | --- | --- | +| Identidades Git | `git-workdev`, `git-personaldev`, `git-whoami`, `clone-*` y aliases SSH `github-workdev` / `github-personaldev` | nombre, email, claves y `~/.ssh/config` | +| PCSoft | `is-pcsoft-forbidden`, `is-pcsoft-editable`, `edit` | IDE Windows y estado del proyecto | +| Worktrees | `wtadd`, `wtlist`, `wtremove`; `wtadd` bloquea repos PCSoft antes de mutar | directorios de worktree y procesos locales | + +`edit` rechaza extensiones PCSoft prohibidas y pide confirmación para las editables. `wtremove` elimina solamente el worktree nombrado: verificá `wtlist` antes de usarlo. Home Manager mapea cada función explícitamente; no administra el directorio completo, claves, remotes, historial ni variables privadas. + +Para validar sin tocar identidades reales ni la red: + +```bash +/opt/homebrew/bin/fish fish/tests/w3-helpers.fish +``` + +## Helpers Fish: screenshots (W4b) + +W4b agrega `ss`, `last`, `ssd` e `imgclip` sin capturar la pantalla ni leer el clipboard durante la carga. `SCREENSHOTS_DIR` usa el override solo si apunta a un directorio existente; si no, conserva el fallback de macOS: `~/Screenshots` cuando existe y luego `~/Desktop`. Home Manager mapea cada función explícitamente y no administra el directorio, screenshots ni clipboard del host. + +Para validar con `HOME`, `PATH` y comandos macOS falsos aislados: + +```bash +/opt/homebrew/bin/fish fish/tests/w4b-screenshots.fish +``` + +## Helpers Fish de agentes (W6) + +**ADVERTENCIA:** por elección explícita del owner, `cc` ejecuta Claude Code con `--dangerously-skip-permissions` y `oc`/`ocb` ejecutan OpenCode con `--auto`; estos shortcuts intencionalmente omiten o autoaprueban permisos. W6 agrega `cc`, `oc`, `ocb`, `ccx`, `ccd` y `ccclip` como helpers Fish opt-in en el directorio validado; `ccx` entrega el contexto por stdin y `ccclip` escribe al clipboard solo al invocarse. No incluye `ccb`. + +| Área | Owner en W6 | +| --- | --- | +| Helpers y soporte privado | `fish/functions/{_cortex_resolve_target,_cortex_run_agent,cc,oc,ocb,ccx,ccd,ccclip}.fish` | +| Home Manager | Mapea cada una de esas funciones de forma explícita; no administra binarios de agentes, clipboard, worktrees, estado, historial ni configuración de proveedores | + +Para validar con agentes y clipboard falsos aislados: + +```bash +/opt/homebrew/bin/fish fish/tests/w6-agent-helpers.fish +``` + ## Estructura ``` @@ -108,7 +221,7 @@ La coordinación vigente del contrato shell se sigue en [cortex #1259](https://g ## Comandos principales | Comando | Descripción | -|---------|-------------| +| --------- | ------------- | | `gs`, `ga`, `gc`, `gp`, `gl` | Git shortcuts | | `dev`, `barbat`, `cowork`, `personal`, `tools`, `worktrees` | Navegación rápida en `~/dev` | | `work`, `innit`, `innit-apis`, `innit-mobile`, `innit-webs`, `innit-pcsoft` | Navegación rápida de trabajo | @@ -131,6 +244,7 @@ La coordinación vigente del contrato shell se sigue en [cortex #1259](https://g ## Personalización Editá `local/env.zsh` (gitignored) para configurar: + - `SCREENSHOTS_DIR` — directorio de screenshots - `WORKSPACE_DIR` — directorio raíz de tus proyectos - `BARBATDEV_DIR` — repos de barbatdev, por defecto `$WORKSPACE_DIR/barbatdev` @@ -147,6 +261,8 @@ Editá `local/env.zsh` (gitignored) para configurar: Referencia completa: [Herdr workflow](docs/herdr-workflow.md). Atajos prácticos: [keymaps](docs/keymaps.md). +El profile Fish también expone `h`, `hs`, `hl`, `hhere`, `hmain`, `hrole`, `hnew`, `hfocus`, `hside`, `hscratch`, `hname`, `whereami`, `sshc`, `sshx` y `sshx-doctor`; Home Manager mapea cada función de forma explícita. + Usá `hremote` desde tu terminal local en macOS. No hagas `ssh` primero y después intentes levantar `herdr` dentro de esa sesión remota. - Para pegar una imagen del clipboard local en la terminal remota, usá `Ctrl+V` por defecto (no `Cmd+V`). @@ -161,7 +277,7 @@ La config macOS enlaza `sketchybar/` en `~/.config/sketchybar`. El diseño es so 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, Ghostty/Herdr y Claude de formato vertical | brand + panel/spaces + app activa; derecha: RAM + CPU + hora | | LG Ultrawide (`display=3`) | mixto: Ghostty/Herdr, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa; derecha: RAM + CPU + hora | @@ -172,7 +288,7 @@ El centro queda libre para evitar el notch y reducir ruido visual. Interacciones: | Item | Acción | -|------|--------| +| ------ | -------- | | Glyph RefactorIA | abre `~/dev` | | Spaces | enfocan el space si `yabai` está corriendo | | Volumen | mute/unmute | @@ -216,7 +332,7 @@ El instalador crea symlinks en ambas rutas de configuración: `~/.config/yabai/y Decisiones: | Tema | Decisión | -|------|----------| +| ------ | ---------- | | Leader | `Option + Command` | | Scripting addition | No se usa | | Raycast | Evitar shortcuts con `Option + Command` para reducir colisiones | @@ -253,7 +369,7 @@ Binarios a permitir: Atajos principales (`option + command`): | Atajo | Acción | -|-------|--------| +| ------- | -------- | | `Option + Command + Left/Down/Up/Right` | Focus izquierda/abajo/arriba/derecha | | `Option + Command + Shift + Left/Down/Up/Right` | Mover ventana en el layout | | `Option + Command + 1..9` | Ir al space | @@ -279,7 +395,7 @@ La config enlaza `karabiner/karabiner.json` en `~/.config/karabiner/karabiner.js Remap incluido: | Tecla | Acción | -|-------|--------| +| ------- | -------- | | `Caps Lock` tap | `Escape` | | `Caps Lock` hold | `Left Control` | | `Right Command` | `Delete backward` | @@ -300,7 +416,7 @@ Después de instalar o cambiar permisos, puede hacer falta abrir o reiniciar Kar El prompt usa una variante local de FiraCode Nerd Font Mono con el glyph de la barba de RefactorIA en el Private Use Area. | Dato | Valor | -|------|-------| +| ------ | ------- | | Family | `FiraCode Nerd Font Mono Beard` | | Archivo instalado | `~/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf` | | Codepoint | `U+F0F00` | @@ -343,6 +459,7 @@ refactoria ``` Uso recomendado: + - screenshots o demos donde conviene una marca visual sin depender de imágenes - intros manuales antes de grabar o compartir una terminal - banners puntuales en scripts propios, siempre que no tapen output útil diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish new file mode 100644 index 0000000..2ea1e16 --- /dev/null +++ b/fish/conf.d/10-core.fish @@ -0,0 +1,73 @@ +# Core Fish defaults. Host overrides load later from 99-local.fish. + +if not set -q WORKSPACE_DIR; or test -z "$WORKSPACE_DIR" + set -gx WORKSPACE_DIR "$HOME/dev" +end +if not set -q BARBATDEV_DIR; or test -z "$BARBATDEV_DIR" + set -gx BARBATDEV_DIR "$WORKSPACE_DIR/barbatdev" +end +if not set -q CORTEX_HOME; or test -z "$CORTEX_HOME" + set -gx CORTEX_HOME "$HOME/.cortex" +end +if not set -q CORTEX_ROOT; or test -z "$CORTEX_ROOT" + set -gx CORTEX_ROOT "$BARBATDEV_DIR/cortex/cortex" +end +if not set -q CORTEX_DOTFILES_DIR; or test -z "$CORTEX_DOTFILES_DIR" + set -gx CORTEX_DOTFILES_DIR "$BARBATDEV_DIR/cortex/cortex-dotfiles" +end +if not set -q CORTEX_MULTIPLEXER; or test -z "$CORTEX_MULTIPLEXER" + set -gx CORTEX_MULTIPLEXER cmux +end +set -gx CLAUDE_CODE_EFFORT_LEVEL high +if not set -q WORK_PROJECTS_DIR; or test -z "$WORK_PROJECTS_DIR" + set -gx WORK_PROJECTS_DIR "$BARBATDEV_DIR/innit" +end +if not set -q PERSONAL_PROJECTS_DIR; or test -z "$PERSONAL_PROJECTS_DIR" + set -gx PERSONAL_PROJECTS_DIR "$BARBATDEV_DIR/products" +end +if not set -q INNIT_DIR; or test -z "$INNIT_DIR" + set -gx INNIT_DIR "$BARBATDEV_DIR/innit" +end +if not set -q SCREENSHOTS_DIR; or test -z "$SCREENSHOTS_DIR" + set -gx SCREENSHOTS_DIR "$HOME/Screenshots" +end + +if command -q nvim + set -gx EDITOR (command -s nvim) +else if command -q vim + set -gx EDITOR (command -s vim) +else + set -gx EDITOR nano +end +set -gx VISUAL "$EDITOR" + +alias g git +alias gs 'git status' +alias ga 'git add' +alias gc 'git commit -m' +alias gp 'git push' +alias gl 'git log --oneline --graph --decorate --all' +alias gd 'git diff' +alias gco 'git checkout' +alias gb 'git branch' +alias gpl 'git pull' +alias gf 'git fetch' +alias gst 'git stash' +alias .. 'cd ..' +alias ... 'cd ../..' +alias .... 'cd ../../..' +alias c clear +alias o open + +if command -q eza + alias ll 'eza -la --icons --git' + alias la 'eza -la --icons --git --all' + alias lt 'eza --tree --icons --level=2' +else + alias ll 'ls -lah' + alias la 'ls -lah -A' +end + +if status is-interactive; and command -q starship + starship init fish | source +end diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example new file mode 100644 index 0000000..1e46392 --- /dev/null +++ b/fish/conf.d/99-local.fish.example @@ -0,0 +1,10 @@ +# Copy to ~/.config/fish/conf.d/99-local.fish and keep that file untracked. +# This loads after 10-core.fish and is for machine-specific overrides. +# set -gx WORKSPACE_DIR "$HOME/dev" +# set -gx INNIT_APIS_DIR "$HOME/dev/innit/apis" +# set -gx EDITOR nvim +# Private Git identities: both values are required before git-workdev or git-personaldev can change a repository. +# set -gx GIT_WORKDEV_NAME "Your work name" +# set -gx GIT_WORKDEV_EMAIL "you@company.invalid" +# set -gx GIT_PERSONALDEV_NAME "Your personal name" +# set -gx GIT_PERSONALDEV_EMAIL "you@personal.invalid" diff --git a/fish/functions/_cortex_resolve_target.fish b/fish/functions/_cortex_resolve_target.fish new file mode 100644 index 0000000..72eeb40 --- /dev/null +++ b/fish/functions/_cortex_resolve_target.fish @@ -0,0 +1,11 @@ +function _cortex_resolve_target + set -l target "$argv[1]" + test -n "$target"; or set target . + + if not test -d "$target" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + + cd "$target"; and pwd +end diff --git a/fish/functions/_cortex_run_agent.fish b/fish/functions/_cortex_run_agent.fish new file mode 100644 index 0000000..ff5cefd --- /dev/null +++ b/fish/functions/_cortex_run_agent.fish @@ -0,0 +1,11 @@ +function _cortex_run_agent + set -l target "$argv[1]" + set -e argv[1] + set -l original_dir "$PWD" + + cd "$target"; or return 1 + command $argv + set -l agent_status $status + cd "$original_dir"; or return 1 + return "$agent_status" +end diff --git a/fish/functions/_git_config_identity.fish b/fish/functions/_git_config_identity.fish new file mode 100644 index 0000000..63109e4 --- /dev/null +++ b/fish/functions/_git_config_identity.fish @@ -0,0 +1,23 @@ +function _git_config_identity --argument-names name email alias label route_marker + if test -z "$name"; or test -z "$email" + printf 'Missing private %s identity: set name and email in 99-local.fish.\n' "$label" >&2 + return 1 + end + + git rev-parse --is-inside-work-tree >/dev/null 2>&1; or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + + git config --local user.name "$name"; and git config --local user.email "$email"; or return 1 + set -l url (git remote get-url origin 2>/dev/null) + if string match -q '*github.com*' -- "$url"; or string match -q "*$route_marker*" -- "$url" + set -l new_url (string replace -r '^git@github[^:]*:' "git@$alias:" -- "$url") + if test "$new_url" != "$url" + git remote set-url origin "$new_url"; or return 1 + printf 'Remote updated: %s\n' "$new_url" + end + end + + printf 'Identity: %s (%s)\n' "$label" "$email" +end diff --git a/fish/functions/_go_dev_dir.fish b/fish/functions/_go_dev_dir.fish new file mode 100644 index 0000000..b12d0e3 --- /dev/null +++ b/fish/functions/_go_dev_dir.fish @@ -0,0 +1,8 @@ +function _go_dev_dir --argument-names target + if test -d "$target" + cd "$target" + return 0 + end + printf 'Directory not found: %s\n' "$target" >&2 + return 1 +end diff --git a/fish/functions/_go_first_existing_dir.fish b/fish/functions/_go_first_existing_dir.fish new file mode 100644 index 0000000..47f0e94 --- /dev/null +++ b/fish/functions/_go_first_existing_dir.fish @@ -0,0 +1,10 @@ +function _go_first_existing_dir + for target in $argv + if test -d "$target" + cd "$target" + return 0 + end + end + printf 'Directory not found: %s\n' "$argv[1]" >&2 + return 1 +end diff --git a/fish/functions/_parse_github_repo.fish b/fish/functions/_parse_github_repo.fish new file mode 100644 index 0000000..4e54519 --- /dev/null +++ b/fish/functions/_parse_github_repo.fish @@ -0,0 +1,5 @@ +function _parse_github_repo --argument-names input + set input (string replace -r '\.git$' '' -- "$input") + set input (string replace -r '^https://github\.com/' '' -- "$input") + string replace -r '^git@github[^:]*:' '' -- "$input" +end diff --git a/fish/functions/_screenshot_files.fish b/fish/functions/_screenshot_files.fish new file mode 100644 index 0000000..c77d08b --- /dev/null +++ b/fish/functions/_screenshot_files.fish @@ -0,0 +1,20 @@ +function _screenshot_files + set -l directory (_screenshots_dir) + set -l files + + # BSD find lacks GNU -maxdepth, so retain the source helper's two-level scope in Fish. + for file in (command find "$directory" -type f -print0 2>/dev/null | string split0) + set -l relative (string replace -- "$directory/" '' "$file") + set -l components (string split / -- "$relative") + test (count $components) -le 2; or continue + + switch (path extension "$file") + case .png .jpg .jpeg .gif + set -a files "$file" + end + end + + if test (count $files) -gt 0 + command ls -t $files 2>/dev/null + end +end diff --git a/fish/functions/_screenshots_dir.fish b/fish/functions/_screenshots_dir.fish new file mode 100644 index 0000000..17cfdcc --- /dev/null +++ b/fish/functions/_screenshots_dir.fish @@ -0,0 +1,13 @@ +function _screenshots_dir + if set -q SCREENSHOTS_DIR; and test -n "$SCREENSHOTS_DIR"; and test -d "$SCREENSHOTS_DIR" + printf '%s\n' "$SCREENSHOTS_DIR" + return + end + + set -l default "$HOME/Screenshots" + if test -d "$default" + printf '%s\n' "$default" + else + printf '%s\n' "$HOME/Desktop" + end +end diff --git a/fish/functions/_time_ago.fish b/fish/functions/_time_ago.fish new file mode 100644 index 0000000..ea9236a --- /dev/null +++ b/fish/functions/_time_ago.fish @@ -0,0 +1,20 @@ +function _time_ago + set -l file "$argv[1]" + set -l now (command date +%s) + set -l mtime (command stat -f %m "$file" 2>/dev/null) + + if test $status -ne 0; or test -z "$mtime" + set mtime "$now" + end + + set -l difference (math "$now - $mtime") + if test "$difference" -lt 60 + printf 'justo ahora\n' + else if test "$difference" -lt 3600 + printf '%sm ago\n' (math "$difference / 60") + else if test "$difference" -lt 86400 + printf '%sh ago\n' (math "$difference / 3600") + else + printf '%sd ago\n' (math "$difference / 86400") + end +end diff --git a/fish/functions/_wt_git_root.fish b/fish/functions/_wt_git_root.fish new file mode 100644 index 0000000..bd001ac --- /dev/null +++ b/fish/functions/_wt_git_root.fish @@ -0,0 +1,3 @@ +function _wt_git_root + git rev-parse --show-toplevel 2>/dev/null +end diff --git a/fish/functions/_wt_is_pcsoft_repo.fish b/fish/functions/_wt_is_pcsoft_repo.fish new file mode 100644 index 0000000..427f491 --- /dev/null +++ b/fish/functions/_wt_is_pcsoft_repo.fish @@ -0,0 +1,4 @@ +function _wt_is_pcsoft_repo + set -l root (_wt_git_root); or return 1 + git -C "$root" ls-files 2>/dev/null | string match -rqi '\.(wdp|wwp|wpp|wpj|wwh|wpw|prw|wdd|fic|mmo|ndx|wdr|wdq|wdi|wdt|wdv|wdk|rep|sty|cpl|bkp|wdg|wdc|wde|wdw)$' +end diff --git a/fish/functions/_wt_path_for.fish b/fish/functions/_wt_path_for.fish new file mode 100644 index 0000000..d0ca87e --- /dev/null +++ b/fish/functions/_wt_path_for.fish @@ -0,0 +1,3 @@ +function _wt_path_for --argument-names git_root name + printf '%s/dev/worktrees/%s/%s\n' "$HOME" (path basename "$git_root") "$name" +end diff --git a/fish/functions/barbat.fish b/fish/functions/barbat.fish new file mode 100644 index 0000000..5ffde99 --- /dev/null +++ b/fish/functions/barbat.fish @@ -0,0 +1,4 @@ +# Usage: barbat — change to the Barbatdev workspace. +function barbat + _go_dev_dir "$BARBATDEV_DIR" +end diff --git a/fish/functions/cc.fish b/fish/functions/cc.fish new file mode 100644 index 0000000..99531aa --- /dev/null +++ b/fish/functions/cc.fish @@ -0,0 +1,5 @@ +# Usage: cc [path] +function cc + set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 + _cortex_run_agent "$resolved" claude --dangerously-skip-permissions +end diff --git a/fish/functions/ccclip.fish b/fish/functions/ccclip.fish new file mode 100644 index 0000000..db8f35b --- /dev/null +++ b/fish/functions/ccclip.fish @@ -0,0 +1,43 @@ +# Usage: ccclip [-n|--line-numbers] +function ccclip + if test (count $argv) -eq 0 + printf 'Uso: ccclip [archivo2 ...] [-n|--line-numbers]\n' >&2 + return 1 + end + + set -l with_numbers false + set -l files + for arg in $argv + switch "$arg" + case -n --line-numbers + set with_numbers true + case '*' + set -a files "$arg" + end + end + + begin + for file in $files + if not test -f "$file" + printf 'Archivo no encontrado: %s\n' "$file" >&2 + continue + end + + set -l extension (path extension "$file" | string trim -c .) + printf '```%s\n// File: %s\n' "$extension" "$file" + set -l content + if test "$with_numbers" = true + set content (nl -ba "$file" | string collect) + else + set content (command cat "$file" | string collect) + end + printf '%s\n```\n\n' "$content" + end + end | pbcopy + set -l clipboard_status $pipestatus[-1] + if test "$clipboard_status" -ne 0 + return "$clipboard_status" + end + + printf 'Contexto copiado al clipboard (%d archivos)\n' (count $files) +end diff --git a/fish/functions/ccd.fish b/fish/functions/ccd.fish new file mode 100644 index 0000000..9f3fd98 --- /dev/null +++ b/fish/functions/ccd.fish @@ -0,0 +1,16 @@ +# Usage: ccd [subpath] +function ccd + set -l subpath "$argv[1]" + set -l workspace "$WORKSPACE_DIR" + test -n "$workspace"; or set workspace "$HOME/dev" + set -l target "$workspace" + test -n "$subpath"; and set target "$workspace/$subpath" + + if test -d "$target" + cd "$target" + printf 'Navegando a: %s\n' "$target" + else + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end +end diff --git a/fish/functions/ccx.fish b/fish/functions/ccx.fish new file mode 100644 index 0000000..7292837 --- /dev/null +++ b/fish/functions/ccx.fish @@ -0,0 +1,14 @@ +# Usage: ccx [path] +function ccx + set -l context "$argv[1]" + if test -z "$context" + printf 'Uso: ccx [directorio]\n' >&2 + return 1 + end + + set -l resolved (_cortex_resolve_target "$argv[2]"); or return 1 + begin + printf '%s\n' "$context" | _cortex_run_agent "$resolved" claude + return $pipestatus[-1] + end +end diff --git a/fish/functions/clone-personaldev.fish b/fish/functions/clone-personaldev.fish new file mode 100644 index 0000000..9a9fe9f --- /dev/null +++ b/fish/functions/clone-personaldev.fish @@ -0,0 +1,8 @@ +function clone-personaldev + if test (count $argv) -lt 1 + printf 'Usage: clone-personaldev \n' >&2 + return 1 + end + set -l repo (_parse_github_repo "$argv[1]") + git clone "git@github-personaldev:$repo.git" +end diff --git a/fish/functions/clone-workdev.fish b/fish/functions/clone-workdev.fish new file mode 100644 index 0000000..818255e --- /dev/null +++ b/fish/functions/clone-workdev.fish @@ -0,0 +1,8 @@ +function clone-workdev + if test (count $argv) -lt 1 + printf 'Usage: clone-workdev \n' >&2 + return 1 + end + set -l repo (_parse_github_repo "$argv[1]") + git clone "git@github-workdev:$repo.git" +end diff --git a/fish/functions/cowork.fish b/fish/functions/cowork.fish new file mode 100644 index 0000000..42fda9d --- /dev/null +++ b/fish/functions/cowork.fish @@ -0,0 +1,4 @@ +# Usage: cowork — change to the work projects directory. +function cowork + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" +end diff --git a/fish/functions/dev.fish b/fish/functions/dev.fish new file mode 100644 index 0000000..fe30b96 --- /dev/null +++ b/fish/functions/dev.fish @@ -0,0 +1,4 @@ +# Usage: dev — change to the development workspace. +function dev + _go_dev_dir "$HOME/dev" +end diff --git a/fish/functions/dotfiles.fish b/fish/functions/dotfiles.fish new file mode 100644 index 0000000..69f0dae --- /dev/null +++ b/fish/functions/dotfiles.fish @@ -0,0 +1,8 @@ +# Usage: dotfiles — change to the Cortex dotfiles repository. +function dotfiles + if set -q _DOTFILES_DIR; and test -n "$_DOTFILES_DIR" + _go_dev_dir "$_DOTFILES_DIR" + else + _go_dev_dir "$CORTEX_DOTFILES_DIR" + end +end diff --git a/fish/functions/edit.fish b/fish/functions/edit.fish new file mode 100644 index 0000000..c3a8ac2 --- /dev/null +++ b/fish/functions/edit.fish @@ -0,0 +1,19 @@ +function edit + if test (count $argv) -lt 1 + printf 'Usage: edit \n' >&2 + return 1 + end + + set -l target "$argv[1]" + if is-pcsoft-forbidden "$target" + printf 'PCSoft forbidden file: %s\nUse the WinDev/WebDev IDE.\n' "$target" >&2 + return 1 + end + if is-pcsoft-editable "$target" + printf "PCSoft editable file: %s\nOnly edit 'code : |1+' blocks with the IDE closed.\nContinue? [s/N] " "$target" + read -l confirm + string match -qr '^[sS]$' -- "$confirm"; or return 0 + end + + "$EDITOR" "$target" +end diff --git a/fish/functions/git-personaldev.fish b/fish/functions/git-personaldev.fish new file mode 100644 index 0000000..324580b --- /dev/null +++ b/fish/functions/git-personaldev.fish @@ -0,0 +1,3 @@ +function git-personaldev + _git_config_identity "$GIT_PERSONALDEV_NAME" "$GIT_PERSONALDEV_EMAIL" github-personaldev personaldev github-personal +end diff --git a/fish/functions/git-whoami.fish b/fish/functions/git-whoami.fish new file mode 100644 index 0000000..3dd23c5 --- /dev/null +++ b/fish/functions/git-whoami.fish @@ -0,0 +1,6 @@ +function git-whoami + set -l name (git config user.name 2>/dev/null; or echo 'no configurado') + set -l email (git config user.email 2>/dev/null; or echo 'no configurado') + set -l remote (git remote get-url origin 2>/dev/null; or echo 'sin remote') + printf ' nombre : %s\n email : %s\n remote : %s\n' "$name" "$email" "$remote" +end diff --git a/fish/functions/git-workdev.fish b/fish/functions/git-workdev.fish new file mode 100644 index 0000000..2e71198 --- /dev/null +++ b/fish/functions/git-workdev.fish @@ -0,0 +1,3 @@ +function git-workdev + _git_config_identity "$GIT_WORKDEV_NAME" "$GIT_WORKDEV_EMAIL" github-workdev workdev github-work +end diff --git a/fish/functions/h.fish b/fish/functions/h.fish new file mode 100644 index 0000000..6c2caa0 --- /dev/null +++ b/fish/functions/h.fish @@ -0,0 +1,8 @@ +# Run Herdr directly. Usage: h [herdr args...] +function h --description 'Run Herdr directly' + if not command -q herdr + printf 'h: herdr is not available\n' >&2 + return 127 + end + command herdr $argv +end diff --git a/fish/functions/herdr-orient.fish b/fish/functions/herdr-orient.fish new file mode 100644 index 0000000..e66a445 --- /dev/null +++ b/fish/functions/herdr-orient.fish @@ -0,0 +1,20 @@ +# Print the current host, repository, SSH, and Herdr context. Usage: herdr-orient +function herdr-orient --description 'Print the current Herdr context' + printf 'host: %s\n' (hostname -s 2>/dev/null; or hostname) + printf 'cwd: %s\n' "$PWD" + if git rev-parse --is-inside-work-tree >/dev/null 2>&1 + printf 'repo: %s\n' (git rev-parse --show-toplevel) + set -l branch (git branch --show-current 2>/dev/null) + test -n "$branch"; or set branch detached + printf 'branch: %s\n' "$branch" + end + if test -n "$SSH_CONNECTION" + set -l ssh_target $CORTEX_SSH_TARGET + test -n "$ssh_target"; or set ssh_target remote + printf 'ssh: %s\n' "$ssh_target" + end + if command -q herdr; and command -q python3 + set -l pane_id (herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null) + test -n "$pane_id"; and printf 'herdr: %s\n' "$pane_id" + end +end diff --git a/fish/functions/hfocus.fish b/fish/functions/hfocus.fish new file mode 100644 index 0000000..0b6f5f0 --- /dev/null +++ b/fish/functions/hfocus.fish @@ -0,0 +1,4 @@ +# Enter the focused Herdr role session. Usage: hfocus [path] +function hfocus --description 'Enter the focused Herdr role session' + hrole focus $argv +end diff --git a/fish/functions/hhere.fish b/fish/functions/hhere.fish new file mode 100644 index 0000000..233224b --- /dev/null +++ b/fish/functions/hhere.fish @@ -0,0 +1,58 @@ +# Enter the main Herdr session for a path. Usage: hhere [path] +function hhere --description 'Enter the main Herdr session for a path' + if not command -q herdr + printf 'hhere: herdr is not available\n' >&2 + return 127 + end + set -l target $PWD + test (count $argv) -ge 1; and set target $argv[1] + set -l resolved (cd -- "$target" >/dev/null 2>&1; and pwd -P) + if test -z "$resolved" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + set -l host $HOST + if test -z "$host" + set host (hostname -s 2>/dev/null; or hostname) + end + set host (string split -m 1 . -- "$host")[1] + set -l context local + test -n "$SSH_CONNECTION$SSH_CLIENT$SSH_TTY"; and set context ssh + set -l workspace_label (basename "$resolved" | string replace -a . -) + set -l git_root (git -C "$resolved" rev-parse --show-toplevel 2>/dev/null) + if test -n "$git_root" + set -l repo_name (basename "$git_root" | string replace -a . -) + set -l parent_name (dirname "$git_root" | xargs basename | string replace -a . -) + set -l branch (git -C "$git_root" branch --show-current 2>/dev/null) + set workspace_label "$parent_name"-"$repo_name" + test -n "$branch"; and set workspace_label "$workspace_label"-(string replace -ra '[^A-Za-z0-9_.-]' - "$branch") + end + set -l session "$context"-"$host"-"$workspace_label" + if test "$HERDR_ENV" = 1 + if not command -q python3 + printf 'hhere: python3 is not available\n' >&2 + return 127 + end + set -l workspace_id (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 +' "$workspace_label" 2>/dev/null) + if test -n "$workspace_id" + herdr workspace focus "$workspace_id" >/dev/null; or return + else + herdr workspace create --cwd "$resolved" --label "$workspace_label" --focus >/dev/null; or return + end + printf 'workspace: %s\n' "$workspace_label" + return 0 + end + env CORTEX_MULTIPLEXER=herdr herdr --session "$session" +end diff --git a/fish/functions/hl.fish b/fish/functions/hl.fish new file mode 100644 index 0000000..587d57a --- /dev/null +++ b/fish/functions/hl.fish @@ -0,0 +1,4 @@ +# List Herdr workspaces. Usage: hl [herdr args...] +function hl --description 'List Herdr workspaces' + h workspace list $argv +end diff --git a/fish/functions/hmain.fish b/fish/functions/hmain.fish new file mode 100644 index 0000000..c6f592f --- /dev/null +++ b/fish/functions/hmain.fish @@ -0,0 +1,4 @@ +# Enter the main Herdr session for a path. Usage: hmain [path] +function hmain --description 'Enter the main Herdr session for a path' + hhere $argv +end diff --git a/fish/functions/hname.fish b/fish/functions/hname.fish new file mode 100644 index 0000000..93ae153 --- /dev/null +++ b/fish/functions/hname.fish @@ -0,0 +1,30 @@ +# Rename the current Herdr pane. Usage: hname [label] +function hname --description 'Rename the current Herdr pane' + if not command -q herdr + printf 'hname: herdr is not available\n' >&2 + return 127 + end + if not command -q python3 + printf 'hname: python3 is not available\n' >&2 + return 127 + end + set -l label $argv[1] + if test -z "$label" + set label (basename "$PWD" | string replace -a . -) + set -l git_root (git rev-parse --show-toplevel 2>/dev/null) + if test -n "$git_root" + set -l repo_name (basename "$git_root" | string replace -a . -) + set -l parent_name (dirname "$git_root" | xargs basename | string replace -a . -) + set -l branch (git -C "$git_root" branch --show-current 2>/dev/null) + set label "$parent_name"-"$repo_name" + test -n "$branch"; and set label "$label"-(string replace -ra '[^A-Za-z0-9_.-]' - "$branch") + end + end + set -l pane_id (herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null) + if test -z "$pane_id" + printf 'No pude detectar el pane actual de Herdr\n' >&2 + return 1 + end + herdr pane rename "$pane_id" "$label" >/dev/null; or return + printf 'pane: %s\n' "$label" +end diff --git a/fish/functions/hnew.fish b/fish/functions/hnew.fish new file mode 100644 index 0000000..cc2200a --- /dev/null +++ b/fish/functions/hnew.fish @@ -0,0 +1,4 @@ +# Enter a fresh timestamped Herdr session. Usage: hnew [path] +function hnew --description 'Enter a fresh Herdr session' + hrole "new-"(date +%H%M%S)-(random) $argv +end diff --git a/fish/functions/hremote.fish b/fish/functions/hremote.fish new file mode 100644 index 0000000..60ee328 --- /dev/null +++ b/fish/functions/hremote.fish @@ -0,0 +1,20 @@ +# Attach to a remote Herdr session. Usage: hremote [session] +function hremote --description 'Attach to a remote Herdr session' + if test (count $argv) -lt 1 + printf 'Usage: hremote [session]\n' >&2 + return 2 + end + + if not command -q herdr + printf 'hremote: herdr is not available\n' >&2 + return 127 + end + + set -l target $argv[1] + set -l session main + if test (count $argv) -ge 2 + set session $argv[2] + end + + env CORTEX_MULTIPLEXER=herdr CORTEX_SSH_TARGET="$target" herdr --remote "$target" --session "$session" +end diff --git a/fish/functions/hrole.fish b/fish/functions/hrole.fish new file mode 100644 index 0000000..fb6a2f4 --- /dev/null +++ b/fish/functions/hrole.fish @@ -0,0 +1,64 @@ +# Enter a named Herdr role session. Usage: hrole [path] +function hrole --description 'Enter a named Herdr role session' + if test (count $argv) -lt 1; or test -z "$argv[1]" + printf 'Uso: hrole [path]\n' >&2 + return 2 + end + if not command -q herdr + printf 'hrole: herdr is not available\n' >&2 + return 127 + end + set -l role $argv[1] + set -l target $PWD + test (count $argv) -ge 2; and set target $argv[2] + set -l resolved (cd -- "$target" >/dev/null 2>&1; and pwd -P) + if test -z "$resolved" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + set -l host $HOST + if test -z "$host" + set host (hostname -s 2>/dev/null; or hostname) + end + set host (string split -m 1 . -- "$host")[1] + set -l context local + test -n "$SSH_CONNECTION$SSH_CLIENT$SSH_TTY"; and set context ssh + set -l workspace_label (basename "$resolved" | string replace -a . -) + set -l git_root (git -C "$resolved" rev-parse --show-toplevel 2>/dev/null) + if test -n "$git_root" + set -l repo_name (basename "$git_root" | string replace -a . -) + set -l parent_name (dirname "$git_root" | xargs basename | string replace -a . -) + set -l branch (git -C "$git_root" branch --show-current 2>/dev/null) + set workspace_label "$parent_name"-"$repo_name" + test -n "$branch"; and set workspace_label "$workspace_label"-(string replace -ra '[^A-Za-z0-9_.-]' - "$branch") + end + set -l suffix (string replace -ra '[^A-Za-z0-9_.-]' - "$role") + set workspace_label "$workspace_label"-"$suffix" + if test "$HERDR_ENV" = 1 + if not command -q python3 + printf 'hrole: python3 is not available\n' >&2 + return 127 + end + set -l workspace_id (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 +' "$workspace_label" 2>/dev/null) + if test -n "$workspace_id" + herdr workspace focus "$workspace_id" >/dev/null; or return + else + herdr workspace create --cwd "$resolved" --label "$workspace_label" --focus >/dev/null; or return + end + printf 'workspace: %s\n' "$workspace_label" + return 0 + end + env CORTEX_MULTIPLEXER=herdr herdr --session "$context"-"$host"-"$workspace_label" +end diff --git a/fish/functions/hs.fish b/fish/functions/hs.fish new file mode 100644 index 0000000..37778df --- /dev/null +++ b/fish/functions/hs.fish @@ -0,0 +1,4 @@ +# Show Herdr status. Usage: hs [herdr args...] +function hs --description 'Show Herdr status' + h status $argv +end diff --git a/fish/functions/hscratch.fish b/fish/functions/hscratch.fish new file mode 100644 index 0000000..bdde1e1 --- /dev/null +++ b/fish/functions/hscratch.fish @@ -0,0 +1,4 @@ +# Enter the scratch Herdr role session. Usage: hscratch [path] +function hscratch --description 'Enter the scratch Herdr role session' + hrole scratch $argv +end diff --git a/fish/functions/hside.fish b/fish/functions/hside.fish new file mode 100644 index 0000000..1a4b981 --- /dev/null +++ b/fish/functions/hside.fish @@ -0,0 +1,4 @@ +# Enter the side Herdr role session. Usage: hside [path] +function hside --description 'Enter the side Herdr role session' + hrole side $argv +end diff --git a/fish/functions/imgclip.fish b/fish/functions/imgclip.fish new file mode 100644 index 0000000..beb595c --- /dev/null +++ b/fish/functions/imgclip.fish @@ -0,0 +1,21 @@ +# Usage: imgclip copies a supported image to the macOS clipboard. +function imgclip + set -l image "$argv[1]" + + if not test -f "$image" + printf '❌ Archivo no encontrado: %s\n' "$image" + return 1 + end + + set -l extension (string lower -- (string replace -r '^.*\.' '' -- "$image")) + switch "$extension" + case png jpg jpeg gif bmp tiff + set -l resolved (path resolve "$image") + command osascript -e "set the clipboard to (read (POSIX file \"$resolved\") as «class PNGf»)" 2>/dev/null + or command osascript -e "set the clipboard to POSIX file \"$resolved\"" + printf '✓ Imagen copiada al clipboard: %s\n' (command basename "$image") + case '*' + printf '❌ Formato no soportado: %s\n' "$extension" + return 1 + end +end diff --git a/fish/functions/innit-apis.fish b/fish/functions/innit-apis.fish new file mode 100644 index 0000000..d173e2a --- /dev/null +++ b/fish/functions/innit-apis.fish @@ -0,0 +1,4 @@ +# Usage: innit-apis — change to the InnIT APIs directory or fallback. +function innit-apis + _go_first_existing_dir "$INNIT_APIS_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit-mobile.fish b/fish/functions/innit-mobile.fish new file mode 100644 index 0000000..b7d4952 --- /dev/null +++ b/fish/functions/innit-mobile.fish @@ -0,0 +1,4 @@ +# Usage: innit-mobile — change to the InnIT mobile directory or fallback. +function innit-mobile + _go_first_existing_dir "$INNIT_MOBILE_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit-pcsoft.fish b/fish/functions/innit-pcsoft.fish new file mode 100644 index 0000000..19bcf66 --- /dev/null +++ b/fish/functions/innit-pcsoft.fish @@ -0,0 +1,4 @@ +# Usage: innit-pcsoft — change to the InnIT PCSoft directory or fallback. +function innit-pcsoft + _go_first_existing_dir "$INNIT_PCSOFT_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit-webs.fish b/fish/functions/innit-webs.fish new file mode 100644 index 0000000..177066d --- /dev/null +++ b/fish/functions/innit-webs.fish @@ -0,0 +1,4 @@ +# Usage: innit-webs — change to the InnIT web directory or fallback. +function innit-webs + _go_first_existing_dir "$INNIT_WEBS_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit.fish b/fish/functions/innit.fish new file mode 100644 index 0000000..96815c9 --- /dev/null +++ b/fish/functions/innit.fish @@ -0,0 +1,4 @@ +# Usage: innit — change to the first available InnIT directory. +function innit + _go_first_existing_dir "$INNIT_DIR" "$BARBATDEV_DIR/innit" +end diff --git a/fish/functions/is-pcsoft-editable.fish b/fish/functions/is-pcsoft-editable.fish new file mode 100644 index 0000000..fe89a63 --- /dev/null +++ b/fish/functions/is-pcsoft-editable.fish @@ -0,0 +1,4 @@ +function is-pcsoft-editable + set -l extension (string lower -- (path extension "$argv[1]")) + contains -- "$extension" .wdg .wdc .wde .wdw +end diff --git a/fish/functions/is-pcsoft-forbidden.fish b/fish/functions/is-pcsoft-forbidden.fish new file mode 100644 index 0000000..788f743 --- /dev/null +++ b/fish/functions/is-pcsoft-forbidden.fish @@ -0,0 +1,4 @@ +function is-pcsoft-forbidden + set -l extension (string lower -- (path extension "$argv[1]")) + contains -- "$extension" .wdp .wwp .wpp .wpj .wwh .wpw .prw .wdd .fic .mmo .ndx .wdr .wdq .wdi .wdt .wdv .wdk .rep .sty .cpl .bkp .tk .cfg +end diff --git a/fish/functions/last.fish b/fish/functions/last.fish new file mode 100644 index 0000000..5a47dab --- /dev/null +++ b/fish/functions/last.fish @@ -0,0 +1,33 @@ +# Usage: last [-c|--copy] [-o|--open] prints, copies, or opens the latest screenshot. +function last + set -l copy false + set -l open_file false + + for argument in $argv + switch "$argument" + case -c --copy + set copy true + case -o --open + set open_file true + end + end + + set -l directory (_screenshots_dir) + set -l files (_screenshot_files) + set -l file "$files[1]" + + if test -z "$file" + printf '⚠️ No se encontraron screenshots en: %s\n' "$directory" + return 1 + end + + if test "$copy" = true + printf '%s\n' "$file" | command pbcopy + printf '✓ Path copiado: %s\n' (command basename "$file") + else if test "$open_file" = true + command open "$file" + printf '✓ Abriendo: %s\n' (command basename "$file") + else + printf '%s\n' "$file" + end +end diff --git a/fish/functions/oc.fish b/fish/functions/oc.fish new file mode 100644 index 0000000..c0aaaa0 --- /dev/null +++ b/fish/functions/oc.fish @@ -0,0 +1,9 @@ +# Usage: oc [path] +function oc + set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 + set -l flags + if set -q OPENCODE_DEFAULT_FLAGS; and test -n "$OPENCODE_DEFAULT_FLAGS" + set flags (string replace -ra '[[:space:]]+' \n -- "$OPENCODE_DEFAULT_FLAGS") + end + _cortex_run_agent "$resolved" opencode --auto $flags +end diff --git a/fish/functions/ocb.fish b/fish/functions/ocb.fish new file mode 100644 index 0000000..694b2c3 --- /dev/null +++ b/fish/functions/ocb.fish @@ -0,0 +1,4 @@ +# Usage: ocb [path] +function ocb + oc $argv +end diff --git a/fish/functions/personal.fish b/fish/functions/personal.fish new file mode 100644 index 0000000..38212bc --- /dev/null +++ b/fish/functions/personal.fish @@ -0,0 +1,4 @@ +# Usage: personal — change to the personal projects directory. +function personal + _go_first_existing_dir "$PERSONAL_PROJECTS_DIR" "$BARBATDEV_DIR/products" +end diff --git a/fish/functions/ss.fish b/fish/functions/ss.fish new file mode 100644 index 0000000..b0a32e8 --- /dev/null +++ b/fish/functions/ss.fish @@ -0,0 +1,27 @@ +# Usage: ss [count] lists the most recent screenshots. +function ss + set -l count "$argv[1]" + test -n "$count"; or set count 10 + set -l directory (_screenshots_dir) + + if not test -d "$directory" + printf '⚠️ Directorio no encontrado: %s\n' "$directory" + printf ' Configurá SCREENSHOTS_DIR en local/env.zsh\n' + return 1 + end + + printf '\n📸 Últimos %s screenshots en: %s\n\n' "$count" "$directory" + + set -l index 1 + for file in (_screenshot_files) + set -l name (command basename "$file") + set -l size (command du -sh "$file" 2>/dev/null | command cut -f1) + set -l ago (_time_ago "$file") + printf ' [%d] \033[1;37m%s\033[0m \033[90m(%s) — %s\033[0m\n' "$index" "$name" "$size" "$ago" + + set index (math "$index + 1") + test "$index" -le "$count"; or break + end + + printf '\n Usá \033[1;33mlast\033[0m para obtener el path del último screenshot\n\n' +end diff --git a/fish/functions/ssd.fish b/fish/functions/ssd.fish new file mode 100644 index 0000000..4de63cd --- /dev/null +++ b/fish/functions/ssd.fish @@ -0,0 +1,10 @@ +# Usage: ssd opens the configured screenshots directory in Finder. +function ssd + set -l directory (_screenshots_dir) + + if test -d "$directory" + command open "$directory" + else + printf '⚠️ Directorio no encontrado: %s\n' "$directory" + end +end diff --git a/fish/functions/sshc.fish b/fish/functions/sshc.fish new file mode 100644 index 0000000..4d85489 --- /dev/null +++ b/fish/functions/sshc.fish @@ -0,0 +1,18 @@ +# Open a conventional SSH shell with Cortex target context. Usage: sshc [ssh args...] +function sshc --description 'Open SSH with Cortex target context' + if test (count $argv) -lt 1 + printf 'Uso: sshc [ssh args...]\n' >&2 + return 2 + end + set -l target $argv[1] + if string match -q -- '-*' "$target" + printf 'Target SSH inválido: %s\n' "$target" >&2 + return 1 + end + if not command -q ssh + printf 'sshc: ssh is not available\n' >&2 + return 127 + end + set -e argv[1] + env CORTEX_SSH_TARGET="$target" ssh "$target" $argv +end diff --git a/fish/functions/sshx-doctor.fish b/fish/functions/sshx-doctor.fish new file mode 100644 index 0000000..9d06fe7 --- /dev/null +++ b/fish/functions/sshx-doctor.fish @@ -0,0 +1,28 @@ +# Check cmux and SSH host resolution without connecting. Usage: sshx-doctor +function sshx-doctor --description 'Check cmux and SSH host resolution' + if test (count $argv) -lt 1 + printf 'Uso: sshx-doctor \n' >&2 + return 2 + end + set -l target $argv[1] + if string match -q -- '-*' "$target" + printf 'Target SSH inválido: %s\n' "$target" >&2 + return 1 + end + + set -l failed 0 + if not command -q cmux + printf '✗ cmux no encontrado\n' + set failed 1 + end + if not command -q ssh + printf '✗ ssh no encontrado\n' + set failed 1 + else if ssh -G "$target" >/dev/null 2>&1 + printf '✓ SSH config resuelve: %s\n' "$target" + else + printf '✗ SSH config no resuelve: %s\n' "$target" + set failed 1 + end + return $failed +end diff --git a/fish/functions/sshx.fish b/fish/functions/sshx.fish new file mode 100644 index 0000000..37c3fe2 --- /dev/null +++ b/fish/functions/sshx.fish @@ -0,0 +1,22 @@ +# Open a persistent cmux SSH workspace. Usage: sshx [cmux ssh args...] +function sshx --description 'Open persistent cmux SSH workspace' + if test (count $argv) -lt 1 + printf 'Uso: sshx [cmux ssh args...]\n' >&2 + return 2 + end + set -l target $argv[1] + if string match -q -- '-*' "$target" + printf 'Target SSH inválido: %s\n' "$target" >&2 + return 1 + end + if not command -q cmux + printf 'cmux no está disponible; usá sshc para una conexión SSH directa\n' >&2 + return 1 + end + if not command -q ssh + printf 'sshx: ssh is not available\n' >&2 + return 127 + end + set -e argv[1] + env CORTEX_SSH_TARGET="$target" cmux ssh "$target" $argv +end diff --git a/fish/functions/tools.fish b/fish/functions/tools.fish new file mode 100644 index 0000000..8847863 --- /dev/null +++ b/fish/functions/tools.fish @@ -0,0 +1,4 @@ +# Usage: tools — change to the shared tools directory. +function tools + _go_dev_dir "$BARBATDEV_DIR/tools" +end diff --git a/fish/functions/whereami.fish b/fish/functions/whereami.fish new file mode 100644 index 0000000..7adcaf7 --- /dev/null +++ b/fish/functions/whereami.fish @@ -0,0 +1,4 @@ +# Print the current Herdr context. Usage: whereami +function whereami --description 'Print the current Herdr context' + herdr-orient +end diff --git a/fish/functions/work.fish b/fish/functions/work.fish new file mode 100644 index 0000000..3b11466 --- /dev/null +++ b/fish/functions/work.fish @@ -0,0 +1,4 @@ +# Usage: work — change to the first available work projects directory. +function work + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" +end diff --git a/fish/functions/worktrees.fish b/fish/functions/worktrees.fish new file mode 100644 index 0000000..c00a382 --- /dev/null +++ b/fish/functions/worktrees.fish @@ -0,0 +1,4 @@ +# Usage: worktrees — change to the development worktrees directory. +function worktrees + _go_dev_dir "$HOME/dev/worktrees" +end diff --git a/fish/functions/wtadd.fish b/fish/functions/wtadd.fish new file mode 100644 index 0000000..7816ffb --- /dev/null +++ b/fish/functions/wtadd.fish @@ -0,0 +1,37 @@ +function wtadd + set -l name "$argv[1]" + if test -z "$name" + printf 'Usage: wtadd [branch]\n' >&2 + return 1 + end + set -l branch "$name" + if test (count $argv) -ge 2 + set branch "$argv[2]" + end + + set -l root (_wt_git_root); or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + if _wt_is_pcsoft_repo + printf 'PCSoft repository detected — worktrees are not allowed.\n' >&2 + return 1 + end + + set -l worktree (_wt_path_for "$root" "$name") + if test -d "$worktree" + printf 'Worktree already exists: %s\n' "$worktree" >&2 + return 1 + end + mkdir -p (path dirname "$worktree"); or return 1 + if git -C "$root" show-ref --verify --quiet "refs/heads/$branch" + git worktree add "$worktree" "$branch" + else + git worktree add -b "$branch" "$worktree" + end; or return 1 + + printf 'Worktree created: %s\n' "$worktree" + if test "$CORTEX_MULTIPLEXER" = cmux + cc "$worktree" + end +end diff --git a/fish/functions/wtlist.fish b/fish/functions/wtlist.fish new file mode 100644 index 0000000..f13381f --- /dev/null +++ b/fish/functions/wtlist.fish @@ -0,0 +1,9 @@ +function wtlist + _wt_git_root >/dev/null; or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + printf '\n' + git worktree list + printf '\n' +end diff --git a/fish/functions/wtremove.fish b/fish/functions/wtremove.fish new file mode 100644 index 0000000..0b0a732 --- /dev/null +++ b/fish/functions/wtremove.fish @@ -0,0 +1,18 @@ +function wtremove + set -l name "$argv[1]" + if test -z "$name" + printf 'Usage: wtremove \n' >&2 + return 1 + end + set -l root (_wt_git_root); or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + set -l worktree (_wt_path_for "$root" "$name") + if not test -d "$worktree" + printf 'Worktree not found: %s\nUse wtlist to see active worktrees.\n' "$worktree" >&2 + return 1 + end + git worktree remove "$worktree"; and git worktree prune; or return 1 + printf 'Worktree removed: %s\n' "$worktree" +end diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish new file mode 100644 index 0000000..6eafc96 --- /dev/null +++ b/fish/tests/w2-core.fish @@ -0,0 +1,92 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -l core "$repo_root/conf.d/10-core.fish" +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +if test -z "$workspace" + printf 'FAIL: could not create temporary workspace\n' >&2 + exit 1 +end + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +set -g home "$workspace/home" +set -g xdg "$home/.config" +set -g bin "$workspace/bin" +mkdir -p "$xdg/fish/conf.d" "$bin" +printf '#!/bin/sh\n' >"$bin/nvim" +chmod +x "$bin/nvim" + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end + +function run_fish + env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C "$fish_bin" $argv +end + +function assert_navigation_error + set -l command "$argv[1]" + set -l expected "$argv[2]" + set -l description "$argv[3]" + + run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command" >/dev/null 2>"$workspace/error"; and fail "$description must fail" + grep -Fq "Directory not found: $expected" "$workspace/error"; or fail "$description error message" +end + +cp "$core" "$xdg/fish/conf.d/10-core.fish" +set -l defaults (run_fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR $EDITOR $VISUAL') +assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' + +set -l empty_defaults (env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= "$fish_bin" -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') +assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots" 'empty environment values use defaults' + +printf 'set -gx WORKSPACE_DIR "$HOME/private-dev"\nset -gx EDITOR private-editor\nset -gx VISUAL private-editor\n' >"$xdg/fish/conf.d/99-local.fish" +set -l overrides (run_fish -c 'string join "|" $WORKSPACE_DIR $EDITOR $VISUAL') +assert_equal "$overrides" "$home/private-dev|private-editor|private-editor" '99-local override precedence' + +mkdir -p "$home/dev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" +set -l navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/apis'; set -gx INNIT_MOBILE_DIR '$workspace/mobile'; set -gx INNIT_WEBS_DIR '$workspace/webs'; set -gx INNIT_PCSOFT_DIR '$workspace/pcsoft'; set -gx _DOTFILES_DIR '$workspace/dotfiles'; dev; pwd; barbat; pwd; innit; pwd; innit-apis; pwd; innit-mobile; pwd; innit-webs; pwd; innit-pcsoft; pwd; dotfiles; pwd" | string collect) +set -l expected_navigation (printf '%s\n' "$home/dev" "$home/dev/barbatdev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" | string collect) +assert_equal "$navigation" "$expected_navigation" 'navigation destinations' + +set -l fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/missing'; innit-apis; pwd") +assert_equal "$fallback" "$home/dev/barbatdev/innit" 'navigation fallback' +assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing'; barbat" "$workspace/missing" 'barbat missing directory' + +mkdir -p "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" "$home/dev/worktrees" +set -l extended_navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/cowork'; set -gx PERSONAL_PROJECTS_DIR '$workspace/personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; personal; pwd; tools; pwd; worktrees; pwd; work; pwd" | string collect) +set -l expected_extended_navigation (printf '%s\n' "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$home/dev/worktrees" "$workspace/cowork" | string collect) +assert_equal "$extended_navigation" "$expected_extended_navigation" 'extended navigation destinations' + +set -l extended_fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; work; pwd; personal; pwd" | string collect) +set -l expected_extended_fallback (printf '%s\n' "$workspace/barbatdev/innit" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" | string collect) +assert_equal "$extended_fallback" "$expected_extended_fallback" 'extended navigation fallbacks' + +assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; cowork" "$workspace/missing-work" 'cowork missing directories' +assert_navigation_error "set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; personal" "$workspace/missing-personal" 'personal missing directories' +assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tools" "$workspace/missing-barbat/tools" 'tools missing directory' +assert_navigation_error "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" 'worktrees missing directory' +assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" 'work missing directories' + +set -l aliases (run_fish -c 'functions gs; functions gpl' | string collect) +string match -q '*git status*' "$aliases"; or fail 'gs alias' +string match -q '*git pull*' "$aliases"; or fail 'gpl alias' + +printf '#!/bin/sh\n[ "$1" = init ] && printf "set -gx W2_STARSHIP_READY 1\\n"\n' >"$bin/starship" +chmod +x "$bin/starship" +set -l noninteractive (run_fish -c 'set -q W2_STARSHIP_READY; and echo ready; or echo skipped') +assert_equal "$noninteractive" skipped 'noninteractive Starship guard' +set -l interactive (run_fish -i -c 'set -q W2_STARSHIP_READY; and echo ready; or echo skipped' 2>/dev/null) +assert_equal "$interactive" ready 'interactive Starship initialization' + +printf 'PASS: Fish W2 core profile\n' diff --git a/fish/tests/w3-helpers.fish b/fish/tests/w3-helpers.fish new file mode 100644 index 0000000..3c70628 --- /dev/null +++ b/fish/tests/w3-helpers.fish @@ -0,0 +1,118 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +set -g home "$workspace/home" +set -g bin "$workspace/bin" +set -g repo "$workspace/repo" +set -gx W3_GIT_LOG "$workspace/git.log" +mkdir -p "$home" "$bin" "$repo" + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end + +function run_fish + env HOME="$home" PATH="$bin:/usr/bin:/bin" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$workspace/global-gitconfig" "$fish_bin" $argv +end + +printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> "$W3_GIT_LOG"\n[ "$1" = clone ] && exit 0\nexec /usr/bin/git "$@"\n' >"$bin/git" +printf '#!/bin/sh\nprintf editor >> "$W3_EDITOR_LOG"\n' >"$bin/editor" +printf '#!/bin/sh\nprintf ssh >> "$W3_SSH_LOG"\nexit 97\n' >"$bin/ssh" +chmod +x "$bin/git" "$bin/editor" "$bin/ssh" + +/usr/bin/git -C "$repo" init -q +/usr/bin/git -C "$repo" config user.name Baseline +/usr/bin/git -C "$repo" config user.email baseline@example.invalid +/usr/bin/git -C "$repo" remote add origin git@github.com:org/repo.git +printf 'baseline\n' >"$repo/README.md" +/usr/bin/git -C "$repo" add README.md +/usr/bin/git -C "$repo" commit -q -m baseline + +set -l configured (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; set -gx GIT_WORKDEV_NAME Work; set -gx GIT_WORKDEV_EMAIL work@example.invalid; git-workdev >/dev/null; string join '|' (git config --local user.name) (git config --local user.email) (git remote get-url origin)") +assert_equal "$configured" "Work|work@example.invalid|git@github-workdev:org/repo.git" 'work identity and alias routing' + +/usr/bin/git -C "$repo" remote set-url origin git@github-work-legacy:org/repo.git +set -l legacy_alias (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; set -gx GIT_WORKDEV_NAME Work; set -gx GIT_WORKDEV_EMAIL work@example.invalid; git-workdev >/dev/null; git remote get-url origin") +assert_equal "$legacy_alias" git@github-workdev:org/repo.git 'legacy work alias routing' + +/usr/bin/git -C "$repo" remote set-url origin git@github.com:org/repo.git +set -l personal (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; set -gx GIT_PERSONALDEV_NAME Personal; set -gx GIT_PERSONALDEV_EMAIL personal@example.invalid; git-personaldev >/dev/null; string join '|' (git config --local user.name) (git config --local user.email) (git remote get-url origin)") +assert_equal "$personal" "Personal|personal@example.invalid|git@github-personaldev:org/repo.git" 'personal identity and alias routing' + +/usr/bin/git -C "$repo" config --unset-all user.name +/usr/bin/git -C "$repo" config --unset-all user.email +/usr/bin/git -C "$repo" remote set-url origin git@github.com:org/repo.git +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; git-workdev" >/dev/null 2>&1; and fail 'missing private identity must fail' +set -l missing_name (/usr/bin/git -C "$repo" config --local --get user.name) +test -z "$missing_name"; or fail 'missing identity changed local name' +set -l unchanged_remote (/usr/bin/git -C "$repo" remote get-url origin) +assert_equal "$unchanged_remote" git@github.com:org/repo.git 'missing identity changed remote' + +set -l whoami (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; git-whoami") +string match -q '*nombre : no configurado*' "$whoami"; or fail 'git-whoami name fallback' +string match -q '*remote : git@github.com:org/repo.git*' "$whoami"; or fail 'git-whoami remote' + +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; clone-workdev https://github.com/org/work.git; clone-personaldev git@github.com:org/personal.git" +set -l git_log (cat "$workspace/git.log") +string match -q '*clone git@github-workdev:org/work.git*' "$git_log"; or fail "work clone alias: $git_log" +string match -q '*clone git@github-personaldev:org/personal.git*' "$git_log"; or fail 'personal clone alias' + +set -gx W3_EDITOR_LOG "$workspace/editor.log" +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; is-pcsoft-forbidden FILE.WDP"; or fail 'forbidden extension decision' +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; is-pcsoft-editable window.WDW"; or fail 'editable extension decision' +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; is-pcsoft-forbidden window.WDW"; and fail 'editable must not be forbidden' +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx EDITOR editor; edit '$workspace/file.wdp'" >/dev/null 2>&1; and fail 'forbidden edit must fail' +test ! -e "$workspace/editor.log"; or fail 'forbidden edit invoked editor' +printf 'n\n' | run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx EDITOR editor; edit '$workspace/file.wdw'" >/dev/null +test ! -e "$workspace/editor.log"; or fail 'declined editable edit invoked editor' +printf 's\n' | run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx EDITOR editor; edit '$workspace/file.wdw'" >/dev/null +cat "$workspace/editor.log" | grep -qx editor; or fail 'confirmed editable edit' + +printf '' >"$workspace/git.log" +set -l worktree_path "$home/dev/worktrees/repo/w3" +set -l wtadd_output (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx CORTEX_MULTIPLEXER none; cd '$repo'; wtadd w3") +string match -q "*Worktree created: $worktree_path*" "$wtadd_output"; or fail "worktree add output: $wtadd_output" +test -e "$worktree_path/.git"; or fail 'worktree add did not create a linked worktree' +set -l canonical_worktree_path (cd "$worktree_path"; and pwd -P) +/usr/bin/git -C "$repo" show-ref --verify --quiet refs/heads/w3; or fail 'worktree branch was not created' +set -l worktree_list (/usr/bin/git -C "$repo" worktree list --porcelain | string collect) +string match -q "*worktree $canonical_worktree_path*" "$worktree_list"; or fail 'worktree add missing from porcelain list' +string match -q '*branch refs/heads/w3*' "$worktree_list"; or fail 'worktree branch missing from porcelain list' + +set -l wtlist_output (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; wtlist" | string collect) +string match -q "*$canonical_worktree_path*" "$wtlist_output"; or fail 'wtlist missing linked worktree' +set -l worktree_log (string collect <"$workspace/git.log") +string match -q "*worktree add -b w3 $worktree_path*" "$worktree_log"; or fail 'git wrapper missing worktree add command' +string match -q '*worktree list*' "$worktree_log"; or fail 'git wrapper missing worktree list command' + +set -l wtremove_output (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; wtremove w3") +assert_equal "$wtremove_output" "Worktree removed: $worktree_path" 'worktree remove output' +test ! -e "$worktree_path"; or fail 'worktree remove left linked path' +/usr/bin/git -C "$repo" show-ref --verify --quiet refs/heads/w3; or fail 'worktree remove deleted branch' +set -l worktree_list_after (/usr/bin/git -C "$repo" worktree list --porcelain | string collect) +string match -q "*worktree $canonical_worktree_path*" "$worktree_list_after"; and fail 'worktree remove left porcelain entry' +set worktree_log (string collect <"$workspace/git.log") +string match -q "*worktree remove $worktree_path*" "$worktree_log"; or fail 'git wrapper missing worktree remove command' +string match -q '*worktree prune*' "$worktree_log"; or fail 'git wrapper missing worktree prune command' + +printf 'tracked\n' >"$repo/project.wdp" +/usr/bin/git -C "$repo" add project.wdp +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; wtadd blocked" >/dev/null 2>&1; and fail 'PCSoft worktree guard must fail' +test ! -e "$home/dev/worktrees/repo/blocked"; or fail 'PCSoft guard created worktree path' +test ! -e "$workspace/ssh.log"; or fail 'test invoked SSH' + +printf 'PASS: Fish W3 helpers\n' diff --git a/fish/tests/w4b-screenshots.fish b/fish/tests/w4b-screenshots.fish new file mode 100644 index 0000000..e2a1cd2 --- /dev/null +++ b/fish/tests/w4b-screenshots.fish @@ -0,0 +1,160 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end + +function assert_contains + string match -q "*$argv[2]*" "$argv[1]"; or fail "$argv[3] (missing '$argv[2]')" +end + +function run_function + set -l bin_dir "$argv[1]" + set -e argv[1] + set -l isolated_home "$workspace/home" + test -n "$W4B_HOME"; and set isolated_home "$W4B_HOME" + env -i HOME="$isolated_home" PATH="$bin_dir:/usr/bin:/bin" \ + W4B_LOG="$workspace/commands.log" \ + W4B_SCREENSHOTS_DIR="$W4B_SCREENSHOTS_DIR" \ + W4B_OSASCRIPT_FAIL_FIRST="$W4B_OSASCRIPT_FAIL_FIRST" \ + W4B_OSASCRIPT_SEEN="$workspace/osascript.seen" \ + "$fish_bin" --no-config \ + -c 'set -gx fish_function_path $argv[1] $fish_function_path; if test -n "$W4B_SCREENSHOTS_DIR"; set -gx SCREENSHOTS_DIR "$W4B_SCREENSHOTS_DIR"; else; set -e SCREENSHOTS_DIR; end; $argv[2] $argv[3..-1]' \ + "$functions_dir" $argv +end + +set -l fake_bin "$workspace/fake-bin" +set -l empty_bin "$workspace/empty-bin" +set -l home "$workspace/home" +set -l screenshots "$home/Screenshots" +set -l override "$workspace/override" +mkdir -p "$fake_bin" "$empty_bin" "$screenshots/nested/deeper" "$override" + +printf '%s\n' '#!/bin/sh' 'printf "date|%s\n" "$*" >> "$W4B_LOG"' 'printf "1000\n"' >"$fake_bin/date" +printf '%s\n' '#!/bin/sh' 'printf "stat" >> "$W4B_LOG"' 'for arg in "$@"; do printf "|%s" "$arg" >> "$W4B_LOG"; done' 'printf "\n" >> "$W4B_LOG"' 'printf "995\n"' >"$fake_bin/stat" +printf '%s\n' '#!/bin/sh' 'printf "open" >> "$W4B_LOG"' 'for arg in "$@"; do printf "|%s" "$arg" >> "$W4B_LOG"; done' 'printf "\n" >> "$W4B_LOG"' >"$fake_bin/open" +printf '%s\n' '#!/bin/sh' 'input=$(cat)' 'printf "pbcopy|%s\n" "$input" >> "$W4B_LOG"' >"$fake_bin/pbcopy" +printf '%s\n' '#!/bin/sh' 'printf "osascript" >> "$W4B_LOG"' 'for arg in "$@"; do printf "|%s" "$arg" >> "$W4B_LOG"; done' 'printf "\n" >> "$W4B_LOG"' 'if [ "$W4B_OSASCRIPT_FAIL_FIRST" = 1 ] && [ ! -e "$W4B_OSASCRIPT_SEEN" ]; then : > "$W4B_OSASCRIPT_SEEN"; exit 1; fi' >"$fake_bin/osascript" +printf '%s\n' '#!/bin/sh' 'printf "screencapture|%s\n" "$*" >> "$W4B_LOG"' >"$fake_bin/screencapture" +printf '%s\n' '#!/bin/sh' 'printf "pngpaste|%s\n" "$*" >> "$W4B_LOG"' >"$fake_bin/pngpaste" +chmod +x "$fake_bin"/* + +printf 'old\n' >"$screenshots/older.png" +printf 'new\n' >"$screenshots/newer.jpg" +printf 'nested\n' >"$screenshots/nested/latest.gif" +printf 'deep\n' >"$screenshots/nested/deeper/too-deep.png" +printf 'ignored\n' >"$screenshots/ignored.txt" +printf 'override\n' >"$override/override.png" +/usr/bin/touch -t 202401010101 "$screenshots/older.png" +/usr/bin/touch -t 202401010102 "$screenshots/newer.jpg" +/usr/bin/touch -t 202401010103 "$screenshots/nested/latest.gif" +/usr/bin/touch -t 202401010104 "$screenshots/nested/deeper/too-deep.png" +/usr/bin/touch -t 202401010105 "$override/override.png" + +printf '' >"$workspace/commands.log" +set -l listed (run_function "$fake_bin" ss 2 | string collect) +assert_contains "$listed" "Últimos 2 screenshots en: $screenshots" 'ss default directory' +assert_contains "$listed" latest.gif 'ss includes nested image' +assert_contains "$listed" newer.jpg 'ss orders recent image' +assert_contains "$listed" 'justo ahora' 'ss formats recent file age' +string match -q '*older.png*' "$listed"; and fail 'ss honors count' +string match -q '*too-deep.png*' "$listed"; and fail 'ss ignores files deeper than two levels' +assert_contains (string collect <"$workspace/commands.log") "stat|-f|%m|$screenshots/nested/latest.gif" 'ss uses BSD stat arguments' + +set -gx W4B_SCREENSHOTS_DIR "$override" +set listed (run_function "$fake_bin" ss 1 | string collect) +assert_contains "$listed" "Últimos 1 screenshots en: $override" 'ss override directory' +assert_contains "$listed" override.png 'ss override image' +set -e W4B_SCREENSHOTS_DIR + +printf '' >"$workspace/commands.log" +set -l latest (run_function "$fake_bin" last) +assert_equal "$latest" "$screenshots/nested/latest.gif" 'last returns latest image path' + +printf '' >"$workspace/commands.log" +run_function "$fake_bin" last --copy ignored >/dev/null +set -l commands (string collect <"$workspace/commands.log") +assert_contains "$commands" "pbcopy|$screenshots/nested/latest.gif" 'last forwards selected path to pbcopy' +string match -q '*open|*' "$commands"; and fail 'last copy must not open' + +printf '' >"$workspace/commands.log" +run_function "$fake_bin" last --open >/dev/null +set commands (string collect <"$workspace/commands.log") +assert_contains "$commands" "open|$screenshots/nested/latest.gif" 'last forwards selected path to open' +string match -q '*pbcopy|*' "$commands"; and fail 'last open must not copy' + +set -gx W4B_SCREENSHOTS_DIR "$override" +printf '' >"$workspace/commands.log" +run_function "$fake_bin" ssd >/dev/null +set commands (string collect <"$workspace/commands.log") +assert_equal "$commands" "open|$override" 'ssd opens override directory' +set -e W4B_SCREENSHOTS_DIR + +set -gx W4B_SCREENSHOTS_DIR "$workspace/missing" +set -l fallback (run_function "$fake_bin" last) +assert_equal "$fallback" "$screenshots/nested/latest.gif" 'invalid override falls back to default screenshots directory' +set -e W4B_SCREENSHOTS_DIR + +set -gx W4B_HOME "$workspace/no-screen-home" +mkdir -p "$W4B_HOME" +if run_function "$fake_bin" ss >"$workspace/missing.out" 2>&1 + fail 'ss missing default directory must fail' +end +assert_contains (string collect <"$workspace/missing.out") "Directorio no encontrado: $W4B_HOME/Desktop" 'ss missing directory error' +if run_function "$fake_bin" last >"$workspace/last-missing.out" 2>&1 + fail 'last missing default directory must fail' +end +assert_contains (string collect <"$workspace/last-missing.out") "No se encontraron screenshots en: $W4B_HOME/Desktop" 'last missing directory error' +set -l ssd_missing (run_function "$fake_bin" ssd) +assert_contains "$ssd_missing" "Directorio no encontrado: $W4B_HOME/Desktop" 'ssd missing directory error' +mkdir -p "$W4B_HOME/Desktop" +if run_function "$fake_bin" last >"$workspace/last-empty.out" 2>&1 + fail 'last empty directory must fail' +end +assert_contains (string collect <"$workspace/last-empty.out") "No se encontraron screenshots en: $W4B_HOME/Desktop" 'last empty directory error' +set -e W4B_HOME + +set -l resolved_image (path resolve "$screenshots/newer.jpg") +printf '' >"$workspace/commands.log" +run_function "$fake_bin" imgclip "$screenshots/newer.jpg" >/dev/null +set commands (string collect <"$workspace/commands.log") +assert_contains "$commands" "osascript|-e|set the clipboard to (read (POSIX file \"$resolved_image\") as «class PNGf»)" 'imgclip forwards resolved image path' + +printf '' >"$workspace/commands.log" +set -gx W4B_OSASCRIPT_FAIL_FIRST 1 +run_function "$fake_bin" imgclip "$screenshots/newer.jpg" >/dev/null +set commands (string collect <"$workspace/commands.log") +set -l osascript_calls (string match -ra 'osascript\|' -- "$commands") +test (count $osascript_calls) -eq 2; or fail 'imgclip falls back after PNG clipboard attempt' +assert_contains "$commands" "set the clipboard to POSIX file \"$resolved_image\"" 'imgclip fallback arguments' +set -e W4B_OSASCRIPT_FAIL_FIRST + +if run_function "$fake_bin" imgclip "$screenshots/ignored.txt" >"$workspace/unsupported.out" 2>&1 + fail 'imgclip unsupported format must fail' +end +assert_contains (string collect <"$workspace/unsupported.out") 'Formato no soportado: txt' 'imgclip unsupported format error' +if run_function "$fake_bin" imgclip "$workspace/missing.png" >"$workspace/missing-image.out" 2>&1 + fail 'imgclip missing file must fail' +end +assert_contains (string collect <"$workspace/missing-image.out") "Archivo no encontrado: $workspace/missing.png" 'imgclip missing file error' + +set commands (string collect <"$workspace/commands.log") +string match -q '*screencapture|*' "$commands"; and fail 'helpers must not capture the screen' +string match -q '*pngpaste|*' "$commands"; and fail 'helpers must not read the clipboard' + +printf 'PASS: Fish W4b screenshot helpers\n' diff --git a/fish/tests/w5a-hremote.fish b/fish/tests/w5a-hremote.fish new file mode 100644 index 0000000..25eaf0c --- /dev/null +++ b/fish/tests/w5a-hremote.fish @@ -0,0 +1,53 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g function_file "$repo_root/functions/hremote.fish" +set -g workspace (mktemp -d) +if test -z "$workspace" + printf 'FAIL: could not create temporary workspace\n' >&2 + exit 1 +end + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function run_hremote + set -l bin_dir "$argv[1]" + set -e argv[1] + env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" "$fish_bin" --no-config \ + -c 'source $argv[1]; hremote $argv[2..-1]' "$function_file" $argv +end + +set -l empty_bin "$workspace/empty-bin" +set -l fake_bin "$workspace/fake-bin" +mkdir -p "$workspace/home" "$empty_bin" "$fake_bin" + +if run_hremote "$empty_bin" >"$workspace/missing-target.out" 2>&1 + fail 'missing target must fail' +end +set -l missing_target_output (string collect <"$workspace/missing-target.out") +string match -q '*Usage: hremote [session]*' "$missing_target_output"; or fail "missing target usage: $missing_target_output" + +if run_hremote "$empty_bin" agent-dev >"$workspace/missing-herdr.out" 2>&1 + fail 'missing herdr must fail' +end +set -l missing_herdr_output (string collect <"$workspace/missing-herdr.out") +string match -q '*hremote: herdr is not available*' "$missing_herdr_output"; or fail "missing herdr error: $missing_herdr_output" + +printf '%s\n' '#!/bin/sh' 'printf "%s|%s|%s\n" "$CORTEX_MULTIPLEXER" "$CORTEX_SSH_TARGET" "$*"' >"$fake_bin/herdr" +chmod +x "$fake_bin/herdr" + +set -l default_output (run_hremote "$fake_bin" agent-dev) +test "$default_output" = 'herdr|agent-dev|--remote agent-dev --session main'; or fail 'default remote session contract' + +set -l explicit_output (run_hremote "$fake_bin" agent-dev focus) +test "$explicit_output" = 'herdr|agent-dev|--remote agent-dev --session focus'; or fail 'explicit remote session contract' + +printf 'PASS: Fish hremote parity\n' diff --git a/fish/tests/w5b-herdr-ssh.fish b/fish/tests/w5b-herdr-ssh.fish new file mode 100644 index 0000000..f5ac69a --- /dev/null +++ b/fish/tests/w5b-herdr-ssh.fish @@ -0,0 +1,101 @@ +#!/usr/bin/env fish +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function run_function + set -l bin_dir "$argv[1]" + set -e argv[1] + env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" TMPDIR="$workspace/tmp" \ + HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config \ + -c 'set -gx fish_function_path $argv[1]; $argv[2] $argv[3..-1]' "$functions_dir" $argv +end +set -l empty_bin "$workspace/empty-bin" +set -l fake_bin "$workspace/fake-bin" +mkdir -p "$workspace/home" "$workspace/tmp" "$workspace/project" "$empty_bin" "$fake_bin" +set -l resolved_project (cd "$workspace/project"; and pwd -P) +if run_function "$empty_bin" hhere "$workspace/project" >"$workspace/missing-herdr.out" 2>&1 + fail 'hhere without herdr must fail' +end +set -l missing_herdr (string collect <"$workspace/missing-herdr.out") +string match -q '*hhere: herdr is not available*' "$missing_herdr"; or fail "hhere missing herdr: $missing_herdr" +printf '%s\n' '#!/bin/sh' 'printf "herdr|%s|%s|%s\n" "$CORTEX_MULTIPLEXER" "$CORTEX_SSH_TARGET" "$*" >> "$HERDR_TEST_LOG"' 'case "$1 $2" in' ' "workspace list") printf "{\"result\":{\"workspaces\":[]}}\n" ;;' 'esac' >"$fake_bin/herdr" +printf '%s\n' '#!/bin/sh' 'if [ -n "$HERDR_TEST_WORKSPACE_ID" ]; then printf "%s\n" "$HERDR_TEST_WORKSPACE_ID"; elif [ -n "$HERDR_TEST_PANE_ID" ]; then printf "%s\n" "$HERDR_TEST_PANE_ID"; fi' >"$fake_bin/python3" +printf '%s\n' '#!/bin/sh' 'printf "ssh|%s|%s\n" "$CORTEX_SSH_TARGET" "$*" >> "$HERDR_TEST_LOG"' '[ "$1" = -G ] && exit 0' 'exit 99' >"$fake_bin/ssh" +printf '%s\n' '#!/bin/sh' 'printf "cmux|%s|%s\n" "$CORTEX_SSH_TARGET" "$*" >> "$HERDR_TEST_LOG"' >"$fake_bin/cmux" +printf '%s\n' '#!/bin/sh' 'printf "testhost\n"' >"$fake_bin/hostname" +printf '%s\n' '#!/bin/sh' 'printf "123456\n"' >"$fake_bin/date" +chmod +x "$fake_bin/herdr" "$fake_bin/python3" "$fake_bin/ssh" "$fake_bin/cmux" "$fake_bin/hostname" "$fake_bin/date" +set -l local_output (run_function "$fake_bin" hhere "$workspace/project") +test -z "$local_output"; or fail "hhere outer output: $local_output" +set -l herdr_log (string collect <"$workspace/herdr.log") +string match -q "*herdr|herdr||--session local-testhost-project*" "$herdr_log"; or fail "local session: $herdr_log" +printf '' >"$workspace/herdr.log" +env HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; cd $argv[2]; hhere' "$functions_dir" "$workspace/project" +set herdr_log (string collect <"$workspace/herdr.log") +string match -q "*--session local-testhost-project*" "$herdr_log"; or fail "default hhere session: $herdr_log" +printf '' >"$workspace/herdr.log" +set -l focus_output (env HERDR_ENV=1 HERDR_TEST_WORKSPACE_ID=workspace-42 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; hfocus $argv[2]' "$functions_dir" "$workspace/project") +assert_equal "$focus_output" 'workspace: project-focus' 'existing workspace focus output' +set herdr_log (string collect <"$workspace/herdr.log") +string match -q '*workspace list*' "$herdr_log"; or fail "workspace list: $herdr_log" +string match -q '*workspace focus workspace-42*' "$herdr_log"; or fail "workspace focus: $herdr_log" +printf '' >"$workspace/herdr.log" +set -l scratch_output (env HERDR_ENV=1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; hscratch $argv[2]' "$functions_dir" "$workspace/project") +assert_equal "$scratch_output" 'workspace: project-scratch' 'new workspace output' +set herdr_log (string collect <"$workspace/herdr.log") +string match -q "*workspace create --cwd $resolved_project --label project-scratch --focus*" "$herdr_log"; or fail "workspace create: $herdr_log" +if run_function "$fake_bin" hrole >"$workspace/hrole.out" 2>&1 + fail 'hrole without role must fail' +end +string match -q '*Uso: hrole [path]*' (string collect <"$workspace/hrole.out"); or fail 'hrole usage' +if run_function "$fake_bin" hhere "$workspace/missing" >"$workspace/path.out" 2>&1 + fail 'missing path must fail' +end +string match -q "*Directorio no encontrado: $workspace/missing*" (string collect <"$workspace/path.out"); or fail 'missing path error' +printf '' >"$workspace/herdr.log" +run_function "$fake_bin" sshc agent-dev -p 22 +set herdr_log (string collect <"$workspace/herdr.log") +assert_equal "$herdr_log" 'ssh|agent-dev|agent-dev -p 22' 'sshc routing' +printf '' >"$workspace/herdr.log" +run_function "$fake_bin" sshx agent-dev -o StrictHostKeyChecking=no +set herdr_log (string collect <"$workspace/herdr.log") +assert_equal "$herdr_log" 'cmux|agent-dev|ssh agent-dev -o StrictHostKeyChecking=no' 'sshx routing' +printf '' >"$workspace/herdr.log" +run_function "$fake_bin" sshx-doctor agent-dev; or fail 'sshx-doctor success' +set herdr_log (string collect <"$workspace/herdr.log") +assert_equal "$herdr_log" 'ssh||-G agent-dev' 'sshx-doctor must only resolve config' +printf '' >"$workspace/herdr.log" +set -l rename_output (env HERDR_TEST_PANE_ID=pane-1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; hname review' "$functions_dir") +assert_equal "$rename_output" 'pane: review' 'hname output' +set herdr_log (string collect <"$workspace/herdr.log") +string match -q '*pane rename pane-1 review*' "$herdr_log"; or fail "hname routing: $herdr_log" +if run_function "$fake_bin" sshc -bad >"$workspace/invalid.out" 2>&1 + fail 'invalid ssh target must fail' +end +string match -q '*Target SSH inválido: -bad*' (string collect <"$workspace/invalid.out"); or fail 'invalid ssh target error' +if run_function "$empty_bin" sshx agent-dev >"$workspace/missing-cmux.out" 2>&1 + fail 'missing cmux must fail' +end +string match -q '*cmux no está disponible*' (string collect <"$workspace/missing-cmux.out"); or fail 'missing cmux error' +if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; sshc agent-dev' "$functions_dir" >"$workspace/missing-ssh.out" 2>&1 + fail 'missing ssh must fail' +end +string match -q '*sshc: ssh is not available*' (string collect <"$workspace/missing-ssh.out"); or fail 'missing ssh error' +if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; sshx-doctor agent-dev' "$functions_dir" >"$workspace/doctor-missing.out" 2>&1 + fail 'doctor without tools must fail' +end +set -l doctor_missing (string collect <"$workspace/doctor-missing.out") +string match -q '*cmux no encontrado*' "$doctor_missing"; and string match -q '*ssh no encontrado*' "$doctor_missing"; or fail 'doctor missing tools' +printf 'PASS: Fish W5b Herdr and SSH parity\n' diff --git a/fish/tests/w6-agent-helpers.fish b/fish/tests/w6-agent-helpers.fish new file mode 100644 index 0000000..9874454 --- /dev/null +++ b/fish/tests/w6-agent-helpers.fish @@ -0,0 +1,139 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 +function _cleanup_w6 --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' +function contains + string match -q "*$argv[2]*" -- "$argv[1]"; or fail "$argv[3]" +end + +set -g home "$workspace/home" +set -g bin "$workspace/bin" +set -g repo "$workspace/repo" +set -g target "$workspace/target dir" +set -g workspace_dir "$workspace/workspace dir" +mkdir -p "$home/dev" "$bin" "$repo" "$target" "$workspace_dir/sub dir" +printf '#!/bin/sh\nprintf "claude|%%s|cwd=%%s|stdin=" "$*" "$PWD" >> "$W6_LOG"\nwhile IFS= read -r line; do printf "%%s\\n" "$line" >> "$W6_LOG"; done\nprintf "\\n" >> "$W6_LOG"\nexit "${W6_CLAUDE_STATUS:-0}"\n' > "$bin/claude" +printf '#!/bin/sh\nprintf "opencode|%%s|cwd=%%s\\n" "$*" "$PWD" >> "$W6_LOG"\nexit "${W6_OPENCODE_STATUS:-0}"\n' > "$bin/opencode" +printf '#!/bin/sh\nprintf "pbcopy|" >> "$W6_LOG"\nwhile IFS= read -r line; do printf "%%s\\n" "$line" >> "$W6_LOG"; done\nprintf "\\n" >> "$W6_LOG"\nexit "${W6_PBCOPY_STATUS:-0}"\n' > "$bin/pbcopy" +printf '#!/bin/sh\nfor file; do while IFS= read -r line || [ -n "$line" ]; do printf "%%s\\n" "$line"; done < "$file"; done\n' > "$bin/cat" +printf '%s\n' '#!/bin/sh' 'for file; do' ' case "$file" in -*) continue ;; esac' ' i=0' ' while IFS= read -r line || [ -n "$line" ]; do' ' i=$((i + 1))' ' printf "%6s\\t%s\\n" "$i" "$line"' ' done < "$file"' 'done' > "$bin/nl" +chmod +x "$bin"/* +printf 'alpha\n' > "$target/code file.fish" +printf 'beta\n' > "$target/other.txt" +printf '' > "$target/empty.txt" + +function run_w6 + env -i HOME="$home" PATH="$bin:/usr/bin:/bin" W6_LOG="$workspace/log" \ + W6_CLAUDE_STATUS="$W6_CLAUDE_STATUS" W6_OPENCODE_STATUS="$W6_OPENCODE_STATUS" W6_PBCOPY_STATUS="$W6_PBCOPY_STATUS" OPENCODE_DEFAULT_FLAGS="$OPENCODE_DEFAULT_FLAGS" WORKSPACE_DIR="$WORKSPACE_DIR" \ + "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$functions_dir" "$repo" $argv +end +function run_w6_unavailable + env -i HOME="$home" PATH="/usr/bin:/bin" "$fish_bin" --no-config \ + -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$functions_dir" "$repo" $argv +end + +printf '' > "$workspace/log" +run_w6 cc; or fail 'cc must use the current directory without arguments' +string match -q -- "claude|--dangerously-skip-permissions|cwd=$repo|stdin=" (string collect <"$workspace/log"); or fail 'cc default command order' +printf '' > "$workspace/log" +run_w6 cc "$target"; or fail 'cc must succeed' +set -l log (string collect <"$workspace/log") +string match -q -- "claude|--dangerously-skip-permissions|cwd=$target|stdin=" "$log"; or fail 'cc target command order' + +printf '' > "$workspace/log" +run_w6 oc "$target"; or fail 'oc must succeed without defaults' +string match -q -- "opencode|--auto|cwd=$target" (string collect <"$workspace/log"); or fail 'oc default command order' +printf '' > "$workspace/log" +set -gx OPENCODE_DEFAULT_FLAGS (printf '%s\t%s\n%s' --model test --plain | string collect --no-trim-newlines) +run_w6 oc "$target"; or fail 'oc must tokenize shell whitespace in defaults' +string match -q -- "opencode|--auto --model test --plain|cwd=$target" (string collect <"$workspace/log"); or fail 'oc tokenizes defaults after one auto flag' +set -gx OPENCODE_DEFAULT_FLAGS '--model test --plain' +printf '' > "$workspace/log" +run_w6 ocb "$target"; or fail 'ocb must succeed' +string match -q -- "opencode|--auto --model test --plain|cwd=$target" (string collect <"$workspace/log"); or fail 'ocb forwards defaults after one auto flag' +set -e OPENCODE_DEFAULT_FLAGS +set -gx W6_OPENCODE_STATUS 24 +run_w6 oc "$target" >/dev/null 2>&1; and fail 'opencode status must propagate' +set -e W6_OPENCODE_STATUS + +printf '' > "$workspace/log" +run_w6 ccx 'initial context with spaces' "$target"; or fail 'ccx must succeed' +contains (string collect <"$workspace/log") "claude||cwd=$target|stdin=initial context with spaces" 'ccx forwards context through stdin' +if run_w6 ccx >"$workspace/ccx.out" 2>&1 + fail 'ccx without context must fail' +end +contains (string collect <"$workspace/ccx.out") 'Uso: ccx' 'ccx usage' + +if run_w6 cc "$workspace/missing" >"$workspace/missing.out" 2>&1 + fail 'missing agent target must fail' +end +contains (string collect <"$workspace/missing.out") 'Directorio no encontrado' 'missing target error' +set -gx W6_CLAUDE_STATUS 23 +run_w6 cc "$target" >/dev/null 2>&1; and fail 'claude status must propagate' +set -e W6_CLAUDE_STATUS +set -gx W6_CLAUDE_STATUS 25 +run_w6 ccx context "$target" >/dev/null 2>&1; and fail 'ccx status must propagate' +set -e W6_CLAUDE_STATUS +if run_w6_unavailable cc "$target" >"$workspace/unavailable.out" 2>&1 + fail 'unavailable claude must fail' +end +contains (string collect <"$workspace/unavailable.out") 'claude' 'unavailable claude identifies command' + +set -gx WORKSPACE_DIR '' +set -l ccd_default (run_w6 ccd) +contains "$ccd_default" "Navegando a: $home/dev" 'ccd default directory' +set -gx WORKSPACE_DIR "$workspace_dir" +set -l ccd_output (run_w6 ccd 'sub dir') +contains "$ccd_output" "Navegando a: $workspace_dir/sub dir" 'ccd uses workspace override' +source "$functions_dir/ccd.fish" +cd "$repo" +ccd 'sub dir' >/dev/null; or fail 'ccd must change the invoking Fish process directory' +test "$PWD" = "$workspace_dir/sub dir"; or fail 'ccd persists PWD in the invoking Fish process' +if run_w6 ccd missing >"$workspace/ccd.out" 2>&1 + fail 'ccd missing directory must fail' +end +contains (string collect <"$workspace/ccd.out") 'Directorio no encontrado' 'ccd validates target' +set -e WORKSPACE_DIR + +printf '' > "$workspace/log" +run_w6 ccclip "$target/code file.fish" -n; or fail 'ccclip must copy content' +set log (string collect <"$workspace/log") +contains "$log" "pbcopy|```fish" 'ccclip fences extension' +contains "$log" "// File: $target/code file.fish" 'ccclip preserves spaced path' +contains "$log" '1 alpha' 'ccclip includes line numbers' +printf '' > "$workspace/log" +run_w6 ccclip "$target/code file.fish"; or fail 'ccclip must copy newline-terminated content' +set log (string collect <"$workspace/log") +set -l expected_fence (string join \n alpha '```' | string collect) +contains "$log" "$expected_fence" 'ccclip trims trailing linefeeds before the closing fence' +printf '' > "$workspace/log" +run_w6 ccclip "$target/empty.txt"; or fail 'ccclip must copy empty files' +printf '' > "$workspace/log" +run_w6 ccclip "$workspace/missing"; or fail 'ccclip empty context preserves successful clipboard status' +contains (string collect <"$workspace/log") 'pbcopy|' 'ccclip hands empty context to clipboard' +set -gx W6_PBCOPY_STATUS 31 +run_w6 ccclip "$target/other.txt" >/dev/null 2>&1; and fail 'clipboard failure must propagate' +set -e W6_PBCOPY_STATUS +if run_w6 ccclip >"$workspace/clip.out" 2>&1 + fail 'ccclip without files must fail' +end +contains (string collect <"$workspace/clip.out") 'Uso: ccclip' 'ccclip usage' + +for file in "$functions_dir/_cortex_resolve_target.fish" "$functions_dir/_cortex_run_agent.fish" "$functions_dir/ocb.fish" "$functions_dir/ccx.fish" "$functions_dir/ccd.fish" "$functions_dir/ccclip.fish" + set -l source (string collect < "$file") + string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv|--yes)' "$source"; and fail "unexpected unsafe W6 source: $file" +end + +printf 'PASS: Fish W6 agent helpers\n' diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..52a2fda --- /dev/null +++ b/flake.lock @@ -0,0 +1,49 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1747688870, + "narHash": "sha256-ypL9WAZfmJr5V70jEVzqGjjQzF0uCkz+AFQF7n9NmNc=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "d5f1f641b289553927b3801580598d200a501863", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-24.11", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9983641 --- /dev/null +++ b/flake.nix @@ -0,0 +1,30 @@ +{ + description = "Inert Home Manager boundary for cortex-dotfiles"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + home-manager = { + url = "github:nix-community/home-manager/release-24.11"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, home-manager, ... }: + let + mkHomeConfiguration = { username, homeDirectory, system }: + home-manager.lib.homeManagerConfiguration { + pkgs = nixpkgs.legacyPackages.${system}; + extraSpecialArgs = { + inherit username homeDirectory system; + }; + modules = [ ./nix/home.nix ]; + }; + in + { + homeConfigurations.jbarbat = mkHomeConfiguration { + username = "jbarbat"; + homeDirectory = "/Users/jbarbat"; + system = "aarch64-darwin"; + }; + }; +} diff --git a/nix/home.nix b/nix/home.nix new file mode 100644 index 0000000..c4b0cb9 --- /dev/null +++ b/nix/home.nix @@ -0,0 +1,86 @@ +{ username, homeDirectory, system, ... }: + +{ + assertions = [ + { + assertion = username != "" && homeDirectory != ""; + message = "homeConfigurations.jbarbat must provide explicit non-empty username and homeDirectory values."; + } + ]; + + home = { + username = username; + homeDirectory = homeDirectory; + stateVersion = "24.11"; + }; + + programs.home-manager.enable = true; + + xdg.configFile = { + "nix/nix.conf".text = "experimental-features = nix-command flakes\n"; + "fish/conf.d/10-core.fish".source = ../fish/conf.d/10-core.fish; + "fish/functions/_go_dev_dir.fish".source = ../fish/functions/_go_dev_dir.fish; + "fish/functions/_go_first_existing_dir.fish".source = ../fish/functions/_go_first_existing_dir.fish; + "fish/functions/dev.fish".source = ../fish/functions/dev.fish; + "fish/functions/barbat.fish".source = ../fish/functions/barbat.fish; + "fish/functions/cowork.fish".source = ../fish/functions/cowork.fish; + "fish/functions/personal.fish".source = ../fish/functions/personal.fish; + "fish/functions/tools.fish".source = ../fish/functions/tools.fish; + "fish/functions/worktrees.fish".source = ../fish/functions/worktrees.fish; + "fish/functions/work.fish".source = ../fish/functions/work.fish; + "fish/functions/innit.fish".source = ../fish/functions/innit.fish; + "fish/functions/innit-apis.fish".source = ../fish/functions/innit-apis.fish; + "fish/functions/innit-mobile.fish".source = ../fish/functions/innit-mobile.fish; + "fish/functions/innit-webs.fish".source = ../fish/functions/innit-webs.fish; + "fish/functions/innit-pcsoft.fish".source = ../fish/functions/innit-pcsoft.fish; + "fish/functions/dotfiles.fish".source = ../fish/functions/dotfiles.fish; + "fish/functions/_parse_github_repo.fish".source = ../fish/functions/_parse_github_repo.fish; + "fish/functions/_git_config_identity.fish".source = ../fish/functions/_git_config_identity.fish; + "fish/functions/clone-workdev.fish".source = ../fish/functions/clone-workdev.fish; + "fish/functions/clone-personaldev.fish".source = ../fish/functions/clone-personaldev.fish; + "fish/functions/git-workdev.fish".source = ../fish/functions/git-workdev.fish; + "fish/functions/git-personaldev.fish".source = ../fish/functions/git-personaldev.fish; + "fish/functions/git-whoami.fish".source = ../fish/functions/git-whoami.fish; + "fish/functions/is-pcsoft-forbidden.fish".source = ../fish/functions/is-pcsoft-forbidden.fish; + "fish/functions/is-pcsoft-editable.fish".source = ../fish/functions/is-pcsoft-editable.fish; + "fish/functions/edit.fish".source = ../fish/functions/edit.fish; + "fish/functions/_wt_git_root.fish".source = ../fish/functions/_wt_git_root.fish; + "fish/functions/_wt_is_pcsoft_repo.fish".source = ../fish/functions/_wt_is_pcsoft_repo.fish; + "fish/functions/_wt_path_for.fish".source = ../fish/functions/_wt_path_for.fish; + "fish/functions/wtadd.fish".source = ../fish/functions/wtadd.fish; + "fish/functions/wtlist.fish".source = ../fish/functions/wtlist.fish; + "fish/functions/wtremove.fish".source = ../fish/functions/wtremove.fish; + "fish/functions/hremote.fish".source = ../fish/functions/hremote.fish; + "fish/functions/h.fish".source = ../fish/functions/h.fish; + "fish/functions/hs.fish".source = ../fish/functions/hs.fish; + "fish/functions/hl.fish".source = ../fish/functions/hl.fish; + "fish/functions/hhere.fish".source = ../fish/functions/hhere.fish; + "fish/functions/hmain.fish".source = ../fish/functions/hmain.fish; + "fish/functions/hrole.fish".source = ../fish/functions/hrole.fish; + "fish/functions/hnew.fish".source = ../fish/functions/hnew.fish; + "fish/functions/hfocus.fish".source = ../fish/functions/hfocus.fish; + "fish/functions/hside.fish".source = ../fish/functions/hside.fish; + "fish/functions/hscratch.fish".source = ../fish/functions/hscratch.fish; + "fish/functions/hname.fish".source = ../fish/functions/hname.fish; + "fish/functions/herdr-orient.fish".source = ../fish/functions/herdr-orient.fish; + "fish/functions/whereami.fish".source = ../fish/functions/whereami.fish; + "fish/functions/sshc.fish".source = ../fish/functions/sshc.fish; + "fish/functions/sshx.fish".source = ../fish/functions/sshx.fish; + "fish/functions/sshx-doctor.fish".source = ../fish/functions/sshx-doctor.fish; + "fish/functions/_screenshots_dir.fish".source = ../fish/functions/_screenshots_dir.fish; + "fish/functions/_screenshot_files.fish".source = ../fish/functions/_screenshot_files.fish; + "fish/functions/_time_ago.fish".source = ../fish/functions/_time_ago.fish; + "fish/functions/ss.fish".source = ../fish/functions/ss.fish; + "fish/functions/last.fish".source = ../fish/functions/last.fish; + "fish/functions/ssd.fish".source = ../fish/functions/ssd.fish; + "fish/functions/imgclip.fish".source = ../fish/functions/imgclip.fish; + "fish/functions/_cortex_resolve_target.fish".source = ../fish/functions/_cortex_resolve_target.fish; + "fish/functions/_cortex_run_agent.fish".source = ../fish/functions/_cortex_run_agent.fish; + "fish/functions/cc.fish".source = ../fish/functions/cc.fish; + "fish/functions/oc.fish".source = ../fish/functions/oc.fish; + "fish/functions/ocb.fish".source = ../fish/functions/ocb.fish; + "fish/functions/ccx.fish".source = ../fish/functions/ccx.fish; + "fish/functions/ccd.fish".source = ../fish/functions/ccd.fish; + "fish/functions/ccclip.fish".source = ../fish/functions/ccclip.fish; + }; +} diff --git a/scripts/nix-preflight.sh b/scripts/nix-preflight.sh new file mode 100755 index 0000000..5adc9b4 --- /dev/null +++ b/scripts/nix-preflight.sh @@ -0,0 +1,97 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +failures=0 + +pass() { + printf 'PASS %s\n' "$1" +} + +warn() { + printf 'WARN %s\n' "$1" +} + +fail() { + printf 'FAIL %s\n' "$1" + failures=$((failures + 1)) +} + +username="$(id -un)" +home_directory="$HOME" +host_os="$(uname -s)" +host_architecture="$(uname -m)" + +if [[ -n "$username" ]]; then + pass "Current username: $username" +else + fail 'Current username is unavailable.' +fi + +if [[ "$home_directory" == /* && -d "$home_directory" ]]; then + pass "Current home directory: $home_directory" +else + fail "HOME must be an existing absolute directory; received: $home_directory" +fi + +if [[ "$host_os" == "Darwin" ]]; then + pass 'Host platform: Darwin.' +else + fail "W1 currently targets macOS; detected platform: $host_os" +fi + +case "$host_architecture" in +arm64 | x86_64) + pass "Supported macOS architecture: $host_architecture" + ;; +*) + fail "Unsupported macOS architecture for W1: $host_architecture" + ;; +esac + +if grep -Fq 'home-manager' "$repo_root/flake.nix"; then + pass 'Home Manager is declared by this repository flake; no standalone home-manager command is required.' +else + fail 'Home Manager is not declared by flake.nix.' +fi + +if ! command -v nix >/dev/null 2>&1; then + fail 'Nix is unavailable; install Nix outside this repository before the future bootstrap.' +else + pass 'Nix is available.' + + if nix --extra-experimental-features 'nix-command flakes' flake metadata --no-write-lock-file --offline "$repo_root" >/dev/null 2>&1; then + pass 'Flake features and the local lock/input metadata are ready for read-only evaluation.' + + if nix --extra-experimental-features 'nix-command flakes' eval --no-write-lock-file --offline --raw "$repo_root#homeConfigurations.jbarbat.activationPackage.drvPath" >/dev/null 2>&1; then + pass 'Pure Home Manager configuration jbarbat is available through the flake input.' + else + fail 'Pure Home Manager configuration jbarbat cannot be evaluated locally; generate flake.lock during the future bootstrap, then retry.' + fi + else + fail 'Flake metadata is not available offline; generate flake.lock during the future bootstrap, then retry.' + fi +fi + +fish_config="$home_directory/.config/fish" +local_fish_config="$fish_config/conf.d/99-local.fish" + +if [[ -e "$fish_config" || -L "$fish_config" ]]; then + warn "Future Fish config path exists and will remain unmanaged: $fish_config" +else + pass "Future Fish config path is absent: $fish_config" +fi + +if [[ -e "$local_fish_config" || -L "$local_fish_config" ]]; then + warn "Future host-owned path exists and will remain unmanaged: $local_fish_config" +else + pass "Reserved host-owned future path is absent: $local_fish_config" +fi + +if [[ "$failures" -eq 0 ]]; then + pass 'W1 readiness complete: this check made no host changes.' + exit 0 +fi + +printf 'FAIL W1 readiness is incomplete: this check made no host changes.\n' +exit 1 diff --git a/scripts/test-nix-preflight.sh b/scripts/test-nix-preflight.sh new file mode 100755 index 0000000..ca5f5b1 --- /dev/null +++ b/scripts/test-nix-preflight.sh @@ -0,0 +1,127 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +preflight="$repo_root/scripts/nix-preflight.sh" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +fail() { + printf 'FAIL %s\n' "$1" >&2 + exit 1 +} + +assert_contains() { + local expected="$1" + local file="$2" + + grep -Fqx "$expected" "$file" || fail "expected output: $expected" +} + +assert_not_contains() { + local forbidden="$1" + local file="$2" + + if grep -Fq -- "$forbidden" "$file"; then + fail "forbidden W1 construct found in $file: $forbidden" + fi +} + +assert_pure_w1_configuration() { + assert_contains ' mkHomeConfiguration = { username, homeDirectory, system }:' "$repo_root/flake.nix" + assert_contains ' homeConfigurations.jbarbat = mkHomeConfiguration {' "$repo_root/flake.nix" + assert_contains ' username = "jbarbat";' "$repo_root/flake.nix" + assert_contains ' homeDirectory = "/Users/jbarbat";' "$repo_root/flake.nix" + assert_contains ' system = "aarch64-darwin";' "$repo_root/flake.nix" + assert_contains '{ username, homeDirectory, system, ... }:' "$repo_root/nix/home.nix" + assert_contains ' - name: Check pure Nix preflight contract' "$repo_root/.github/workflows/ci.yml" + grep -Fq 'La configuración pura de W1 es `homeConfigurations.jbarbat`:' "$repo_root/README.md" || + fail 'README does not document the pure W1 configuration' + + for forbidden in 'home.packages' 'programs.fish' 'services.' 'home.activation'; do + assert_not_contains "$forbidden" "$repo_root/nix/home.nix" + done + + assert_not_contains 'builtins.currentSystem' "$repo_root/flake.nix" + assert_not_contains 'builtins.getEnv' "$repo_root/flake.nix" + + for file in "$repo_root/scripts/nix-preflight.sh" "$repo_root/.github/workflows/ci.yml" "$repo_root/README.md"; do + assert_not_contains '--impure' "$file" + done +} + +make_fake_commands() { + local bin_dir="$1" + + mkdir -p "$bin_dir" + + cat >"$bin_dir/id" <<'EOF' +#!/bin/sh +if [ "$1" = "-un" ]; then + printf '%s\n' 'test-user' +fi +EOF + + cat >"$bin_dir/uname" <<'EOF' +#!/bin/sh +case "$1" in + -s) printf '%s\n' 'Darwin' ;; + -m) printf '%s\n' 'arm64' ;; +esac +EOF + + chmod +x "$bin_dir/id" "$bin_dir/uname" +} + +run_preflight() { + local home_dir="$1" + local bin_dir="$2" + local output="$3" + + HOME="$home_dir" PATH="$bin_dir:/usr/bin:/bin" bash "$preflight" >"$output" 2>&1 +} + +assert_pure_w1_configuration + +missing_nix_home="$tmp_dir/missing-nix-home" +missing_nix_bin="$tmp_dir/missing-nix-bin" +missing_nix_output="$tmp_dir/missing-nix.out" +mkdir -p "$missing_nix_home" "$missing_nix_bin" +make_fake_commands "$missing_nix_bin" + +if run_preflight "$missing_nix_home" "$missing_nix_bin" "$missing_nix_output"; then + fail 'preflight unexpectedly passed without nix' +fi +assert_contains 'FAIL Nix is unavailable; install Nix outside this repository before the future bootstrap.' "$missing_nix_output" +assert_contains 'PASS Home Manager is declared by this repository flake; no standalone home-manager command is required.' "$missing_nix_output" + +ready_home="$tmp_dir/ready-home" +ready_bin="$tmp_dir/ready-bin" +ready_output="$tmp_dir/ready.out" +mkdir -p "$ready_home" "$ready_bin" +make_fake_commands "$ready_bin" +cat >"$ready_bin/nix" <<'EOF' +#!/bin/sh +printf '%s\n' "$*" >>"$NIX_FAKE_LOG" +printf '%s\n' '/nix/store/fake-home-manager-activation.drv' +EOF +chmod +x "$ready_bin/nix" + +NIX_FAKE_LOG="$tmp_dir/nix.log" run_preflight "$ready_home" "$ready_bin" "$ready_output" +assert_contains 'PASS Nix is available.' "$ready_output" +assert_contains 'PASS Flake features and the local lock/input metadata are ready for read-only evaluation.' "$ready_output" +assert_contains 'PASS Pure Home Manager configuration jbarbat is available through the flake input.' "$ready_output" +grep -F -- '--no-write-lock-file' "$tmp_dir/nix.log" >/dev/null || fail 'preflight did not prevent lock writes' +grep -F -- '--offline' "$tmp_dir/nix.log" >/dev/null || fail 'preflight did not force offline evaluation' +grep -F -- '#homeConfigurations.jbarbat.activationPackage.drvPath' "$tmp_dir/nix.log" >/dev/null || fail 'preflight did not evaluate the explicit pure configuration' +if grep -F -- '--impure' "$tmp_dir/nix.log" >/dev/null; then + fail 'preflight evaluated the configuration impurely' +fi + +mkdir -p "$ready_home/.config/fish/conf.d" +printf '%s\n' '# host-owned local configuration' >"$ready_home/.config/fish/conf.d/99-local.fish" +collision_output="$tmp_dir/collision.out" +NIX_FAKE_LOG="$tmp_dir/nix.log" run_preflight "$ready_home" "$ready_bin" "$collision_output" +assert_contains "WARN Future host-owned path exists and will remain unmanaged: $ready_home/.config/fish/conf.d/99-local.fish" "$collision_output" + +printf '%s\n' 'nix preflight contract ok'