diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b95450..30bfa50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,8 +61,10 @@ jobs: 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 63953d8..3353f52 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,31 @@ Para validar sin tocar identidades reales ni la red: /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 ``` 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/_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/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/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/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/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/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 95e223c..c4b0cb9 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -67,5 +67,20 @@ "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; }; }