diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d51cbd4..30bfa50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,15 @@ jobs: shellcheck --severity=error --shell=bash "${bash_files[@]}" fi - - name: Run Fish W2 harness + - name: Run Fish migration harnesses shell: bash - run: fish fish/tests/w2-core.fish + 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 diff --git a/README.md b/README.md index 5fe44d5..3353f52 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,55 @@ Para validar sin tocar configuración real: 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 ``` @@ -212,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`). diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example index 668ce7a..1e46392 100644 --- a/fish/conf.d/99-local.fish.example +++ b/fish/conf.d/99-local.fish.example @@ -3,3 +3,8 @@ # 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/_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/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/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/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/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/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/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/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/nix/home.nix b/nix/home.nix index c7e078d..c4b0cb9 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -34,5 +34,53 @@ "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; }; }