diff --git a/claude/statusline-command.sh b/claude/statusline-command.sh new file mode 100755 index 0000000..4795618 --- /dev/null +++ b/claude/statusline-command.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Minimal, low-color Claude Code statusline. +# +# Segments (left -> right, most -> least important): +# 1. context window usage — ASCII bar + % (bold; red accent only when high) +# 2. model in use (display_name), with reasoning effort level appended +# when the payload reports one (".effort.level" is only present for +# models that support reasoning effort — it is never fabricated here) +# 3. cwd, short (basename only) +# 4. git branch, with a single "*" marker if the worktree is dirty +# (no ahead/behind, staged/modified/untracked counts — judged noise +# and dropped in favor of a minimal "has changes" cue) +# 5. 5h rate-limit usage (Claude.ai subscription window), when available — +# the one added datum that changes throughout the day and isn't +# redundant with the per-conversation context percentage +# +# No per-segment color theme (Starship-palette replication stays dropped). +# Secondary text uses an explicit gray (not ANSI "dim", which is close to +# unreadable in many terminals). Red accent is reserved for context usage +# only, applied consistently to both the bar and the number. + +input=$(cat) +cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd') +used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') +five_hour_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') +model_name=$(echo "$input" | jq -r '.model.display_name // .model.id // empty') +effort_level=$(echo "$input" | jq -r '.effort.level // empty') + +bold="\033[1m" +gray="\033[38;2;235;235;235m" +accent="\033[1;38;2;255;107;138m" +reset="\033[0m" +bar_width=8 + +parts=() + +# --- 1. context used: ASCII bar + % --- +if [ -n "$used_pct" ]; then + pct_int=$(printf '%.0f' "$used_pct") + filled=$(( (pct_int * bar_width + 50) / 100 )) + [ "$filled" -gt "$bar_width" ] && filled=$bar_width + empty=$(( bar_width - filled )) + + if [ "$pct_int" -ge 80 ]; then + fill_color="$accent" + else + fill_color="$bold" + fi + + filled_str=$(printf '%*s' "$filled" '') + filled_str=${filled_str// /█} + empty_str=$(printf '%*s' "$empty" '') + empty_str=${empty_str// /░} + bar="${gray}[${reset}${fill_color}${filled_str}${reset}${gray}${empty_str}]${reset}" + + parts+=("${bar} ${fill_color}${pct_int}% ctx${reset}") +else + empty_str=$(printf '%*s' "$bar_width" '') + empty_str=${empty_str// /░} + parts+=("${gray}[${empty_str}] ctx --${reset}") +fi + +# --- 2. model in use (+ reasoning effort, only when the payload reports it) --- +if [ -n "$model_name" ]; then + if [ -n "$effort_level" ]; then + parts+=("${gray}${model_name} (${effort_level})${reset}") + else + parts+=("${gray}${model_name}${reset}") + fi +fi + +# --- 3. directory, short --- +parts+=("${gray}$(basename "$cwd")${reset}") + +# --- 4. git branch (+ minimal dirty marker), --no-optional-locks --- +if git -C "$cwd" --no-optional-locks rev-parse --is-inside-work-tree >/dev/null 2>&1; then + branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null) + if [ -n "$branch" ]; then + dirty="" + if [ -n "$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null)" ]; then + dirty="*" + fi + parts+=("${gray}${branch}${dirty}${reset}") + fi +fi + +# --- 5. 5h rate-limit usage, when the field is present --- +if [ -n "$five_hour_pct" ]; then + five_hour_int=$(printf '%.0f' "$five_hour_pct") + parts+=("${gray}5h ${five_hour_int}%${reset}") +fi + +out="" +for i in "${!parts[@]}"; do + [ "$i" -gt 0 ] && out="${out} ${gray}·${reset} " + out="${out}${parts[$i]}" +done + +printf "%b" "$out" diff --git a/claude/themes/refactoria.json b/claude/themes/refactoria.json new file mode 100644 index 0000000..86495a9 --- /dev/null +++ b/claude/themes/refactoria.json @@ -0,0 +1,52 @@ +{ + "name": "RefactorIA", + "base": "dark-daltonized", + "overrides": { + "claude": "#A477FF", + "claudeShimmer": "#A477FF", + "text": "#FFFFFF", + "inverseText": "#0B0714", + "inactive": "#B6B2C0", + "inactiveShimmer": "#E9D5FF", + "subtle": "#918C9C", + "suggestion": "#F8F8F8", + "permission": "#9B7BFF", + "permissionShimmer": "#9B7BFF", + "remember": "#7DD3FC", + "success": "#7DD3FC", + "error": "#EF4444", + "warning": "#FBBF24", + "warningShimmer": "#FFE08A", + "merged": "#C4B5FD", + "promptBorder": "#9B7BFF", + "promptBorderShimmer": "#E9D5FF", + "planMode": "#9B7BFF", + "autoAccept": "#7DD3FC", + "bashBorder": "#FBBF24", + "ide": "#7DD3FC", + "fastMode": "#7DD3FC", + "fastModeShimmer": "#BAE6FD", + "diffAdded": "#0F1D26", + "diffRemoved": "#2A1014", + "diffAddedDimmed": "#0E0D12", + "diffRemovedDimmed": "#0E0D12", + "diffAddedWord": "#7DD3FC", + "diffRemovedWord": "#EF4444", + "userMessageBackground": "#2C2144", + "userMessageBackgroundHover": "#7127FF", + "messageActionsBackground": "#17151D", + "bashMessageBackgroundColor": "#0E0D12", + "memoryBackgroundColor": "#111016", + "selectionBg": "#744BED", + "rate_limit_fill": "#9B7BFF", + "rate_limit_empty": "#17151D", + "briefLabelYou": "#9B7BFF", + "briefLabelClaude": "#9B7BFF", + "blue_FOR_SUBAGENTS_ONLY": "#BAE6FD", + "green_FOR_SUBAGENTS_ONLY": "#7DD3FC", + "cyan_FOR_SUBAGENTS_ONLY": "#7DD3FC", + "yellow_FOR_SUBAGENTS_ONLY": "#FBBF24", + "red_FOR_SUBAGENTS_ONLY": "#EF4444", + "purple_FOR_SUBAGENTS_ONLY": "#C4B5FD" + } +} diff --git a/docs/refactoria-agent-gentle-rollout.md b/docs/refactoria-agent-gentle-rollout.md new file mode 100644 index 0000000..2c56be6 --- /dev/null +++ b/docs/refactoria-agent-gentle-rollout.md @@ -0,0 +1,71 @@ +# RefactorIA CLI visuals on agent-gentle + +The Linux/Fish rollout is **applied and visually approved** on `agent-gentle` (2026-09-27). This is a per-tool manual recipe and recovery record, **not** an all-tool installer. Pi's guarded batch installed only the theme for `pi --use-theme refactoria`. After visual use, its saved default changed from the initially preserved `Gentleman-Sexy` to `refactoria`; the user explicitly chose to **keep RefactorIA as the saved default**. We did not write Pi settings in the guarded batch. No Zsh, Ghostty, Neovim, SketchyBar, Herdr binary patch, package installation, app restart, or broad `install.sh` was involved. + +> **Source availability:** Use the selected feature-branch revision containing the nine Linux replay inputs below. Until those commits are published, an ordinary remote clean clone will not contain them. The Mac `herdr/config.toml` is a separate visual-only change: local onboarding/keys/UI settings must not be copied or staged wholesale. Do not take assets from `main` by assumption, and do not run the rejected `scripts/refactoria_linux.py` or the superseded all-tool draft. + +## Quick path for a future manual replay + +1. Select an **explicit** source tree containing the nine replay files and SHA-256s below. Confirm the destination is the intended Linux account/worktree and use the existing pinned SSH alias only: `ssh -o HostKeyAlias=agent-gentle -o StrictHostKeyChecking=yes -o UpdateHostKeys=no agent-gentle`. +2. Work **one tool at a time** in the order below. Inspect every destination's `lstat` type, mode, owner, SHA-256 or absence; refuse symlinked ancestors and any unexpected baseline. Stage only that tool's source files in a new owned `0700` directory beneath `~/.local/state/refactoria-staging/` and verify source hashes there. +3. Before a target write, create a unique owned `0700` directory beneath `~/.local/state/refactoria-backups/`. Copy each existing exact target as a `0600` file and write `0600` absence markers for targets that do not exist. Check backup hashes, then recheck the entire live baseline. Never print a settings file or its secrets. +4. Generate the candidate for that tool, compare **nonvisual** settings semantically with the backed-up original, parse it and run its isolated checker. Write only approved target paths. Create absent files exclusively (same-directory unique temporary file plus no-clobber link); replace existing files with a same-directory temporary file and rename, preserving the original mode/owner. Recheck immediately before each write. A failed or drifted step stops the tool batch; do not advance automatically. +5. Verify hashes, permissions, parsers, tool-specific runtime checks and a **fresh human visual check** before the next tool. Keep backup and staging intact. To restore, first inspect the live hashes: restore an existing original only when live bytes are the recorded candidate, and remove a newly created file only when its bytes still match the recorded source; skip drifted paths for manual review. Never restore by wildcard or from an unverified backup. A created but now empty `profiles/` or `themes/` directory may intentionally remain after file rollback. + +This procedure is deliberately operator-driven: a script's passing rehearsal does not authorize replacing a different baseline. The current host-specific staged one-off scripts are recovery aids for the **exact** snapshots below, not a portable installer. + +## Approved Linux replay inputs (nine files) + +| Repository path | SHA-256 | +|---|---| +| `herdr/profiles/refactoria.toml` | `db5fb9e9c2cfa2b2c71ec389eaf7dff35596cfeee655740e237013c0d7816e33` | +| `pi/themes/refactoria.json` | `add53fd1744a052af7630508257b0cd6cfc7e53739088370207408f5918347ee` | +| `claude/themes/refactoria.json` | `8f2c3994416a352da5c121ce1831eb65cdbdb59f85324ec8542a2b0489e1020e` | +| `claude/statusline-command.sh` | `8b3524b8f1c9279f19986dbef7be0e3230d9044868d6f21b44b5ad85c40e454c` | +| `opencode/themes/refactoria.json` | `612dd0a82986d8ea9f5478867aed8d6f29e525c3ee37cf2662749a0d2bd6276a` | +| `starship/starship.toml` | `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` | +| `fish/themes/refactoria.fish` | `3b04a53612cf0c0f76dea3b9e6c08f4485c24a451eac46a332ede9d00b5b9583` | +| `fish/functions/fish_greeting.fish` | `15450023712b8c517cd1203e8de636425c97b669d6f3b9f1af403184ef904f6c` | +| `fish/functions/help-profile.fish` | `b079bdc6110cfb55e129cd84654f93951d014b1a7d817df682df21f904a22626` | + +## Applied destinations and recovery boundaries + +All paths below are relative to `/home/jbarbat`; installed files are regular and owned by uid/gid `1000/1000`. The backup root for each row group is `~/.local/state/refactoria-backups//` (directory `0700`, saved files/markers `0600`). **Absent** means no pre-rollout file existed. The approved after-hashes bind a guided restore; never use a backup for a different target identity. + +| Tool / backup `` | Exact destination | Before | Approved after | +|---|---|---|---| +| Herdr / `herdr-_dvfiir9` | `.config/herdr/config.toml` | `d618349e…bc95ca4` | `cc60b9668a7f162516a94bbee3b23c3e3aa8bcfaebc9e90b6eea5e3e2403b945` (0644) | +| | `.config/herdr/profiles/refactoria.toml` | absent | `db5fb9e9…6e33` (0644) | +| Pi / `pi.oZ39kbUB` | `.pi/agent/themes/refactoria.json` | absent | `add53fd17…347ee` (0644) | +| Claude / `claude.7rr9KBTd` | `.claude/settings.json` | `7fe09aff…2323f6` | `3c4e1bbf8ee61dfd7d957193e92683a599b6da81eddd73b84536dae8dd7338ef` (0644) | +| | `.claude/themes/refactoria.json` | absent | `8f2c3994…e1020e` (0644) | +| | `.claude/statusline-command.sh` | absent | `8b3524b8…e454c` (0755) | +| OpenCode / `opencode.qqD2OtS1` | `.config/opencode/opencode.json` | `2f9a1c89…32b3d` | `1dbba72622cb6459b6c75906fc1d9ac352f1c7917d3c039f4ca079ae23315fd6` (0644) | +| | `.config/opencode/tui.json` | `fe85e04e…cb38c8c` | `c450eaa8533ee5a06570dabeacba164e2f51627ebf9ba10a3863409b42bc1c21` (0644) | +| | `.config/opencode/themes/refactoria.json` | absent | `612dd0a8…6276a` (0644) | +| Starship / `starship.M761Rlpy` | `.config/starship.toml` | `978c7203…fc1db` | `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` (0644) | +| Fish / `fish.aT7Q5jpk` | `.config/fish/themes/refactoria.fish` | absent | `3b04a536…b9583` (0644) | +| | `.config/fish/conf.d/zz-refactoria-theme.fish` | **`8ebfb078e66c0c0109dbc1708a4060a7f4a93f13fa2494d41164218c89fdedfb`** | `b0cb6463e40437d772afc12e0b3577428d540528eb5ab2dafb706c8b2a086ea8` (0644) | +| | `.config/fish/functions/fish_greeting.fish` | absent | `15450023…4f6c` (0644) | +| | `.config/fish/functions/help-profile.fish` | absent | `b079bdc6…2626` (0644) | + +**Post-rollout Claude drift:** `.claude/settings.json` now has SHA `cea2d41181e38bd439c919b64fa039d73fae6fa99c41525f94ecc1c69120787e`, not its guarded after-hash in the table. The approved `theme`/`statusLine` remain exact, the five original keys still deep-equal the private backup, and `skipDangerousModePermissionPrompt` was added later (cause unknown). **The staged byte-guarded settings rollback must stop on this drift**; do not restore the old JSON over that key. Any desired reversal of the two visual selectors requires a separately backed-up, reviewed semantic merge. + +**Guarded during our applications:** `.config/fish/config.fish` SHA `9d784d88e78620d459d01c8e385851df6f46d675f28fcfe6eb2db3c7facdfeb5` retains host setup. We wrote **14** target files; `.pi/agent/settings.json` was not one of them and remained SHA `3f3b6489…b900` with `Gentleman-Sexy` immediately after our Pi batch. After visual use, its current SHA is `e41d483e74d48a3ff02594ac4b60be09160110a65284f2880a0078cc965f0067` with `theme="refactoria"`. The user explicitly chose to keep this current default. On another host, choosing a saved default is a **separate guarded settings backup/semantic merge**, not implied by copying the theme; the original current-host settings bytes were not backed up by the Pi theme batch. + +## Per-tool decisions and checks + +| Tool | Candidate construction and verification | Visual result | +|---|---|---| +| Herdr 0.9.1 | Use the portable `herdr/profiles/refactoria.toml` as visual input for `scripts/refactoria_herdr.py:plan_visual_merge`; it modifies only `[theme]` name/auto_switch/custom roles and `[ui] accent` in the existing config, preserving unrelated TOML/comment lines including `[[remote]]`. Validate under private `XDG_CONFIG_HOME` with `herdr config check`, then after apply. Copy the same profile separately. The profile and current Mac config have identical merge input roles; do not take Mac-only settings as Linux input. | Approved in a fresh Herdr session. | +| Pi 0.87.1 | Copy the user theme first; `pi --use-theme refactoria` proves the theme without a settings change. The user later selected `refactoria` as the **saved default**. For a future host, separately back up and semantic-merge just the `theme` key if that default is desired, preserving all other settings; do not assume Pi application itself edits the selector. | Approved in a fresh Pi invocation; saved RefactorIA default retained by user choice. | +| Claude Code 2.1.283 | Deep-copy JSON settings; add `theme="custom:refactoria"` and `statusLine={"type":"command","command":"bash ~/.claude/statusline-command.sh"}` only. Preserve all five other keys; copy theme and executable minimal statusline. `bash -n`, `jq` availability and a synthetic 80%/UTF-8 fixture passed. | Approved in a fresh Claude session. | +| OpenCode 1.18.32 | In `opencode.json`, change `theme="system"` to `"refactoria"`; in `tui.json`, add only `theme="refactoria"`. Preserve all other JSON values and copy theme. | Approved in a fresh OpenCode session. | +| Starship 1.26.0 | Replace **only** the recognized `978c…fc1db` baseline with the source TOML. `starship print-config` and `starship prompt` passed against both an isolated candidate and the live config. | Approved in a fresh Fish shell. | +| Fish 4.9.2 | Copy three sources; replace the exact old full-theme activation with the one-line wrapper `source "$HOME/.config/fish/themes/refactoria.fish"`. Preserve `config.fish`. `fish -n` and an isolated sourced greeting/help invocation passed; unavailable helpers are filtered by `type -q`. | Approved in a fresh Fish shell with `help-profile`. | + +## Unfinished delivery and cleanup + +- At the time of host rollout, no commit, push, merge or full installer ran. The user later authorized **scoped work-unit commits only** on the feature branch. Push/PR/merge remain separate decisions. Repository-local dirty work predates this rollout; do not stage it wholesale. +- Private host backups and per-tool staging remain on `agent-gentle` for recovery. The two misplaced Starship rehearsal fixtures at `/home/jbarbat/dev/refactoria-rehearsal-fixture` and `...fixture2` were removed after explicit user approval and fresh exact-tree validation; the real Starship backup/stage and other isolated private-stage fixtures were retained. +- The rejected all-tool auto script and 323-line Herdr transaction draft, plus their two paired tests, were removed as **four exact untracked files** after the user authorized cleanup. Other drafts and generated bytecode were deliberately left untouched. The only executable rollback helpers for this host are the private staged one-off scripts bound to the backup IDs above; first run their preview and compare hashes, then use the explicit confirmation route if recovery is actually needed. diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index c0a7d60..ea1bef7 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -20,13 +20,21 @@ if not set -q CORTEX_MULTIPLEXER; or test -z "$CORTEX_MULTIPLEXER" end set -gx CLAUDE_CODE_EFFORT_LEVEL high if not set -q WORK_PROJECTS_DIR; or test -z "$WORK_PROJECTS_DIR" - set -gx WORK_PROJECTS_DIR "$BARBATDEV_DIR/innit" + set -gx WORK_PROJECTS_DIR "$WORKSPACE_DIR/innit-sas" end if not set -q PERSONAL_PROJECTS_DIR; or test -z "$PERSONAL_PROJECTS_DIR" set -gx PERSONAL_PROJECTS_DIR "$BARBATDEV_DIR/products" end if not set -q INNIT_DIR; or test -z "$INNIT_DIR" - set -gx INNIT_DIR "$BARBATDEV_DIR/innit" + set -gx INNIT_DIR "$WORKSPACE_DIR/innit-sas" +end + +if not set -q REFACTORIA_DIR; or test -z "$REFACTORIA_DIR" + set -gx REFACTORIA_DIR "$WORKSPACE_DIR/refactoria" +end + +if not set -q GENTLE_DIR; or test -z "$GENTLE_DIR" + set -gx GENTLE_DIR "$WORKSPACE_DIR/gentle" end if not set -q SCREENSHOTS_DIR; or test -z "$SCREENSHOTS_DIR" set -gx SCREENSHOTS_DIR "$HOME/Screenshots" @@ -41,14 +49,6 @@ else end set -gx VISUAL "$EDITOR" -if test (uname) = Linux - for nix_profile_bin in /nix/var/nix/profiles/default/bin "$HOME/.nix-profile/bin" "$HOME/.local/share/mise/shims" "$HOME/.opencode/bin" "$HOME/.local/bin" - if test -d "$nix_profile_bin" - fish_add_path --path --move "$nix_profile_bin" - end - end -end - alias g git alias ga 'git add' alias gc 'git commit -m' @@ -64,10 +64,7 @@ alias .. 'cd ..' alias ... 'cd ../..' alias .... 'cd ../../..' alias c clear - -if test (uname) = Darwin - alias o open -end +alias o open if command -q eza alias ll 'eza -la --icons --git' diff --git a/fish/functions/_go_dev_dir.fish b/fish/functions/_go_dev_dir.fish index b12d0e3..2b9a7ad 100644 --- a/fish/functions/_go_dev_dir.fish +++ b/fish/functions/_go_dev_dir.fish @@ -1,8 +1,3 @@ function _go_dev_dir --argument-names target - if test -d "$target" - cd "$target" - return 0 - end - printf 'Directory not found: %s\n' "$target" >&2 - return 1 + _go_first_existing_dir "$target" end diff --git a/fish/functions/_go_first_existing_dir.fish b/fish/functions/_go_first_existing_dir.fish index 47f0e94..446741e 100644 --- a/fish/functions/_go_first_existing_dir.fish +++ b/fish/functions/_go_first_existing_dir.fish @@ -5,6 +5,18 @@ function _go_first_existing_dir return 0 end end + # No candidate exists: walk up to the nearest existing ancestor of the + # first target so navigation commands degrade gracefully on hosts that + # do not have this part of the workspace layout. + set -l probe "$argv[1]" + while test -n "$probe"; and not test -d "$probe" + set probe (dirname "$probe") + end + if test -n "$probe"; and test -d "$probe" + printf 'Directory not found: %s — nearest existing: %s\n' "$argv[1]" "$probe" >&2 + cd "$probe" + return 0 + end printf 'Directory not found: %s\n' "$argv[1]" >&2 return 1 end diff --git a/fish/functions/cowork.fish b/fish/functions/cowork.fish index 42fda9d..03f87a2 100644 --- a/fish/functions/cowork.fish +++ b/fish/functions/cowork.fish @@ -1,4 +1,4 @@ # Usage: cowork — change to the work projects directory. function cowork - _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$WORKSPACE_DIR/innit-sas" end diff --git a/fish/functions/fish_greeting.fish b/fish/functions/fish_greeting.fish new file mode 100644 index 0000000..b8dde47 --- /dev/null +++ b/fish/functions/fish_greeting.fish @@ -0,0 +1,116 @@ +# Usage: fish_greeting — show the interactive Fish welcome banner. +function fish_greeting + # Presentation roles: success #58E6A8, info #8CC8FF, violet #A477FF, secondary #EBEBEB. + set -l success (set_color --bold '#58E6A8') + set -l info (set_color --bold '#8CC8FF') + set -l violet (set_color --bold '#A477FF') + set -l secondary (set_color '#EBEBEB') + set -l reset (set_color normal) + + set -l terminal_width 80 + if set -q COLUMNS + set -l candidate "$COLUMNS" + if string match -qr '^[1-9][0-9]*$' -- "$candidate" + set terminal_width "$candidate" + end + else if command -q tput + set -l detected_width (tput cols 2>/dev/null) + if string match -qr '^[1-9][0-9]*$' -- "$detected_width" + set terminal_width "$detected_width" + end + end + + set -l title 'Dev Environment' + set -l status_text '⚡ Fish listo' + set -l clock (date '+%H:%M:%S') + + printf '\n' + + if test "$terminal_width" -lt 28 + printf '%s%s%s\n' "$violet" "$title" "$reset" + printf '%s%s%s\n' "$success" "$status_text" "$reset" + printf '%s%s%s\n' "$info" "🕐 $clock" "$reset" + printf '%sEscribí%s\n' "$secondary" "$reset" + printf ' %shelp-profile%s\n' "$violet" "$reset" + if test "$terminal_width" -ge 17 + printf '%spara ver comandos%s\n' "$secondary" "$reset" + printf '%sdisponibles%s\n' "$secondary" "$reset" + else + printf '%spara%s\n' "$secondary" "$reset" + printf '%sver%s\n' "$secondary" "$reset" + printf '%scomandos%s\n' "$secondary" "$reset" + printf '%sdisponibles%s\n' "$secondary" "$reset" + end + printf '\n' + return 0 + end + + set -l frame_width 40 + if test "$terminal_width" -lt 44 + set frame_width (math "$terminal_width - 4") + end + set -l content_width (math "$frame_width - 2") + set -l rule (string repeat -n (math "$frame_width - 2") '═') + + set -l title_length (string length --visible -- "$title") + set -l title_left (math "floor(($content_width - $title_length) / 2)") + test "$title_left" -gt 0; or set title_left 1 + set -l title_right (math "$content_width - $title_length - $title_left") + + set -l status_length (string length --visible -- "$status_text") + set -l status_left (math "floor(($content_width - $status_length) / 2)") + test "$status_left" -gt 0; or set status_left 1 + set -l status_right (math "$content_width - $status_length - $status_left") + + set -l clock_text "🕐 $clock" + set -l clock_length (string length --visible -- "$clock_text") + set -l clock_left (math "floor(($content_width - $clock_length) / 2)") + test "$clock_left" -gt 0; or set clock_left 1 + set -l clock_right (math "$content_width - $clock_length - $clock_left") + + printf '%s ╔%s╗%s\n' "$secondary" "$rule" "$reset" + printf '%s ║%s%s%s%s%s%s║%s\n' \ + "$secondary" \ + (string repeat -n "$title_left" ' ') \ + "$violet" "$title" "$reset" \ + (string repeat -n "$title_right" ' ') \ + "$secondary" "$reset" + printf '%s ╠%s╣%s\n' "$secondary" "$rule" "$reset" + printf '%s ║%s%s%s%s%s%s║%s\n' \ + "$secondary" \ + (string repeat -n "$status_left" ' ') \ + "$success" "$status_text" "$reset" \ + (string repeat -n "$status_right" ' ') \ + "$secondary" "$reset" + printf '%s ║%s%s%s%s%s%s║%s\n' \ + "$secondary" \ + (string repeat -n "$clock_left" ' ') \ + "$info" "$clock_text" "$reset" \ + (string repeat -n "$clock_right" ' ') \ + "$secondary" "$reset" + printf '%s ╚%s╝%s\n' "$secondary" "$rule" "$reset" + printf '\n' + + if test "$terminal_width" -ge 58 + printf ' %sEscribí%s %shelp-profile%s %spara ver comandos disponibles%s\n' \ + "$secondary" "$reset" "$violet" "$reset" "$secondary" "$reset" + else if test "$terminal_width" -ge 40 + printf ' %sEscribí%s %shelp-profile%s\n' "$secondary" "$reset" "$violet" "$reset" + printf ' %spara ver comandos disponibles%s\n' "$secondary" "$reset" + else + printf ' %sEscribí%s\n' "$secondary" "$reset" + printf ' %shelp-profile%s\n' "$violet" "$reset" + if test "$terminal_width" -ge 31 + printf ' %spara ver comandos disponibles%s\n' "$secondary" "$reset" + else if test "$terminal_width" -ge 19 + printf ' %spara ver comandos%s\n' "$secondary" "$reset" + printf ' %sdisponibles%s\n' "$secondary" "$reset" + else + printf ' %spara%s\n' "$secondary" "$reset" + printf ' %sver%s\n' "$secondary" "$reset" + printf ' %scomandos%s\n' "$secondary" "$reset" + printf ' %sdisponibles%s\n' "$secondary" "$reset" + end + end + printf '\n' +end diff --git a/fish/functions/gentle.fish b/fish/functions/gentle.fish new file mode 100644 index 0000000..37bfa91 --- /dev/null +++ b/fish/functions/gentle.fish @@ -0,0 +1,4 @@ +# Usage: gentle — change to the Gentle workspace. +function gentle + _go_dev_dir "$GENTLE_DIR" +end diff --git a/fish/functions/help-profile.fish b/fish/functions/help-profile.fish new file mode 100644 index 0000000..9fc615b --- /dev/null +++ b/fish/functions/help-profile.fish @@ -0,0 +1,224 @@ +# Usage: help-profile — show the confirmed Fish helper commands. +function help-profile + # Presentation roles: info #8CC8FF, violet #A477FF, secondary #EBEBEB, reset normal. + set -l info (set_color --bold '#8CC8FF') + set -l violet (set_color --bold '#A477FF') + set -l secondary (set_color '#EBEBEB') + set -l reset (set_color normal) + + set -l terminal_width 80 + if set -q COLUMNS + set -l candidate "$COLUMNS" + if string match -qr '^[1-9][0-9]*$' -- "$candidate" + set terminal_width "$candidate" + end + else if command -q tput + set -l detected_width (tput cols 2>/dev/null) + if string match -qr '^[1-9][0-9]*$' -- "$detected_width" + set terminal_width "$detected_width" + end + end + + set -l heading_rule '' + set -l heading_indent ' ' + if test "$terminal_width" -ge 26 + set -l rule_width 41 + if test "$terminal_width" -lt 45 + set heading_indent '' + set rule_width (math "$terminal_width - 2") + end + set heading_rule (string repeat -n "$rule_width" '━') + end + + printf '\n' + if test "$terminal_width" -lt 26 + printf '%sFish%s\n' "$violet" "$reset" + if test "$terminal_width" -ge 19 + printf '%s— Referencia rápida%s\n' "$violet" "$reset" + else + printf '%s— Referencia%s\n' "$violet" "$reset" + printf '%srápida%s\n' "$violet" "$reset" + end + else + printf '%s%s%s\n' "$violet" "$heading_rule" "$reset" + printf '%s%sFish — Referencia rápida%s\n' "$violet" "$heading_indent" "$reset" + printf '%s%s%s\n' "$violet" "$heading_rule" "$reset" + end + + printf '\n%sGit identidades%s\n' "$violet" "$reset" + _help_profile_print_entries "$info" "$secondary" "$reset" "$terminal_width" \ + git-workdev 'Setear identidad de trabajo en repo actual' \ + git-personaldev 'Setear identidad personal en repo actual' \ + git-whoami 'Ver identidad y remote configurados' \ + clone-workdev 'Clonar repo de cuenta de trabajo' \ + clone-personaldev 'Clonar repo de cuenta personal' + + printf '\n%sNavegación%s\n' "$violet" "$reset" + _help_profile_print_entries "$info" "$secondary" "$reset" "$terminal_width" \ + dev 'Ir al workspace de desarrollo' \ + barbat 'Ir al workspace de Barbatdev' \ + cowork 'Ir a los proyectos de trabajo compartidos' \ + personal 'Ir a los proyectos personales' \ + tools 'Ir al directorio de herramientas' \ + worktrees 'Ir a los worktrees de desarrollo' \ + work 'Ir a los proyectos de trabajo' \ + innit 'Ir al workspace de InnIT' \ + innit-apis 'Ir al área de APIs de InnIT o alternativa' \ + innit-mobile 'Ir al área mobile de InnIT o alternativa' \ + innit-webs 'Ir al área web de InnIT o alternativa' \ + innit-pcsoft 'Ir al área PCSoft de InnIT o alternativa' \ + dotfiles 'Ir a este repositorio de dotfiles' + + if test "$terminal_width" -ge 17 + printf '\n%sClaude / OpenCode%s\n' "$violet" "$reset" + else + printf '\n%sClaude /%s\n' "$violet" "$reset" + printf '%sOpenCode%s\n' "$violet" "$reset" + end + _help_profile_print_entries "$info" "$secondary" "$reset" "$terminal_width" \ + 'cc [path]' 'Abrir Claude Code en un directorio' \ + 'ccx [path]' 'Abrir Claude Code con contexto inicial' \ + 'ccd [subpath]' 'Ir al workspace de Claude' \ + 'ccclip ' 'Copiar archivos como contexto Markdown' \ + 'oc [path]' 'Abrir OpenCode en un directorio' \ + 'ocb [path]' 'Abrir OpenCode mediante el helper de Fish' \ + herdr-orient 'Ver contexto de host, repo, SSH y Herdr' + + printf '\n' +end + +# Private renderer for command/description pairs; it wraps descriptions on narrow terminals. +function _help_profile_print_entries + set -l info $argv[1] + set -l secondary $argv[2] + set -l reset $argv[3] + set -l terminal_width $argv[4] + set -l entries $argv[5..-1] + set -l description_width (math "$terminal_width - 6") + test "$description_width" -gt 1; or set description_width 1 + + set -l index 1 + set -l entry_count (count $entries) + while test "$index" -lt "$entry_count" + set -l command $entries[$index] + set -l description $entries[(math "$index + 1")] + set -l command_name (string match -r '^[^[:space:]]+' -- "$command") + if not type -q "$command_name" + set index (math "$index + 2") + continue + end + if test "$terminal_width" -ge 72 + printf ' %s%-22s%s %s%s%s\n' "$info" "$command" "$reset" "$secondary" "$description" "$reset" + else + _help_profile_print_command "$info" "$reset" "$terminal_width" "$command" + set -l line '' + for word in (string split ' ' -- "$description") + if test -z "$line" + set line "$word" + else + set -l candidate "$line $word" + if test (string length -- "$candidate") -le "$description_width" + set line "$candidate" + else + printf ' %s%s%s\n' "$secondary" "$line" "$reset" + set line "$word" + end + end + end + test -z "$line"; or printf ' %s%s%s\n' "$secondary" "$line" "$reset" + end + set index (math "$index + 2") + end +end + +# Private renderer for command signatures; it wraps at token and hyphen boundaries. +function _help_profile_print_command + set -l info $argv[1] + set -l reset $argv[2] + set -l terminal_width $argv[3] + set -l command $argv[4] + set -l command_length (string length --visible -- "$command") + + if test (math "$command_length + 2") -le "$terminal_width" + printf ' %s%s%s\n' "$info" "$command" "$reset" + return 0 + end + + set -l line '' + set -l indent ' ' + set -l continuation_indent ' ' + set -l chunk_width (math "$terminal_width - 4") + test "$chunk_width" -gt 0; or set chunk_width 1 + + for token in (string split ' ' -- "$command") + set -l indent_width (string length --visible -- "$indent") + set -l line_length (string length --visible -- "$line") + set -l token_length (string length --visible -- "$token") + set -l separator_width 0 + test -z "$line"; or set separator_width 1 + + if test (math "$indent_width + $line_length + $separator_width + $token_length") -le "$terminal_width" + if test -n "$line" + set line "$line $token" + else + set line "$token" + end + continue + end + + if test -n "$line" + printf '%s%s%s%s\n' "$indent" "$info" "$line" "$reset" + set line '' + set indent "$continuation_indent" + end + + set indent_width (string length --visible -- "$indent") + set token_length (string length --visible -- "$token") + if test (math "$indent_width + $token_length") -le "$terminal_width" + set line "$token" + continue + end + + set -l token_chunks + set -l hyphen_parts (string split '-' -- "$token") + if test (count $hyphen_parts) -gt 1 + set -l part_index 1 + set -l part_count (count $hyphen_parts) + for part in $hyphen_parts + if test "$part_index" -lt "$part_count" + set -a token_chunks "$part-" + else + set -a token_chunks "$part" + end + set part_index (math "$part_index + 1") + end + else + set token_chunks "$token" + end + + set -l safe_chunks + for chunk in $token_chunks + set -l remaining "$chunk" + while test (string length --visible -- "$remaining") -gt "$chunk_width" + set -a safe_chunks (string sub -l "$chunk_width" -- "$remaining") + set remaining (string sub -s (math "$chunk_width + 1") -- "$remaining") + end + set -a safe_chunks "$remaining" + end + + set -l chunk_index 1 + set -l chunk_count (count $safe_chunks) + for chunk in $safe_chunks + if test "$chunk_index" -lt "$chunk_count" + printf '%s%s%s%s\n' "$indent" "$info" "$chunk" "$reset" + set indent "$continuation_indent" + else + set line "$chunk" + set indent "$continuation_indent" + end + set chunk_index (math "$chunk_index + 1") + end + end + + test -z "$line"; or printf '%s%s%s%s\n' "$indent" "$info" "$line" "$reset" +end diff --git a/fish/functions/innit.fish b/fish/functions/innit.fish index 96815c9..33affbe 100644 --- a/fish/functions/innit.fish +++ b/fish/functions/innit.fish @@ -1,4 +1,4 @@ # Usage: innit — change to the first available InnIT directory. function innit - _go_first_existing_dir "$INNIT_DIR" "$BARBATDEV_DIR/innit" + _go_first_existing_dir "$INNIT_DIR" "$WORKSPACE_DIR/innit-sas" end diff --git a/fish/functions/refactoria.fish b/fish/functions/refactoria.fish new file mode 100644 index 0000000..022c59b --- /dev/null +++ b/fish/functions/refactoria.fish @@ -0,0 +1,4 @@ +# Usage: refactoria — change to the RefactorIA workspace. +function refactoria + _go_dev_dir "$REFACTORIA_DIR" +end diff --git a/fish/functions/work.fish b/fish/functions/work.fish index 3b11466..23c1c6e 100644 --- a/fish/functions/work.fish +++ b/fish/functions/work.fish @@ -1,4 +1,4 @@ # Usage: work — change to the first available work projects directory. function work - _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$WORKSPACE_DIR/innit-sas" end diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish index 5d16126..d507628 100644 --- a/fish/tests/w2-core.fish +++ b/fish/tests/w2-core.fish @@ -34,83 +34,58 @@ function run_fish env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C "$fish_bin" $argv end -function run_clean_fish - env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C "$fish_bin" --no-config $argv -end - -function assert_navigation_error +function assert_navigation_fallback set -l command "$argv[1]" - set -l expected "$argv[2]" - set -l description "$argv[3]" - - run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command" >/dev/null 2>"$workspace/error"; and fail "$description must fail" - grep -Fq "Directory not found: $expected" "$workspace/error"; or fail "$description error message" + set -l missing "$argv[2]" + set -l expected_pwd "$argv[3]" + set -l description "$argv[4]" + + set -l pwd_out (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command; pwd" | string collect) + test "$pwd_out" = "$expected_pwd"; or fail "$description must land on nearest existing ancestor (expected '$expected_pwd', got '$pwd_out')" + run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command" 2>"$workspace/error"; and true + grep -Fq "Directory not found: $missing" "$workspace/error"; or fail "$description must warn about the missing directory" + grep -Fq "nearest existing" "$workspace/error"; or fail "$description must name the nearest existing ancestor" end cp "$core" "$xdg/fish/conf.d/10-core.fish" set -l defaults (run_fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR $EDITOR $VISUAL') -assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' +assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/innit-sas|$home/dev/barbatdev/products|$home/dev/innit-sas|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' set -l empty_defaults (env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= "$fish_bin" -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') -assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots" 'empty environment values use defaults' +assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/innit-sas|$home/dev/barbatdev/products|$home/dev/innit-sas|$home/Screenshots" 'empty environment values use defaults' printf 'set -gx WORKSPACE_DIR "$HOME/private-dev"\nset -gx EDITOR private-editor\nset -gx VISUAL private-editor\n' >"$xdg/fish/conf.d/99-local.fish" set -l overrides (run_fish -c 'string join "|" $WORKSPACE_DIR $EDITOR $VISUAL') assert_equal "$overrides" "$home/private-dev|private-editor|private-editor" '99-local override precedence' -mkdir -p "$home/dev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" +mkdir -p "$home/dev" "$home/dev/barbatdev" "$home/dev/innit-sas" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" set -l navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/apis'; set -gx INNIT_MOBILE_DIR '$workspace/mobile'; set -gx INNIT_WEBS_DIR '$workspace/webs'; set -gx INNIT_PCSOFT_DIR '$workspace/pcsoft'; set -gx _DOTFILES_DIR '$workspace/dotfiles'; dev; pwd; barbat; pwd; innit; pwd; innit-apis; pwd; innit-mobile; pwd; innit-webs; pwd; innit-pcsoft; pwd; dotfiles; pwd" | string collect) -set -l expected_navigation (printf '%s\n' "$home/dev" "$home/dev/barbatdev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" | string collect) +set -l expected_navigation (printf '%s\n' "$home/dev" "$home/dev/barbatdev" "$home/dev/innit-sas" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" | string collect) assert_equal "$navigation" "$expected_navigation" 'navigation destinations' set -l fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/missing'; innit-apis; pwd") -assert_equal "$fallback" "$home/dev/barbatdev/innit" 'navigation fallback' -assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing'; barbat" "$workspace/missing" 'barbat missing directory' +assert_equal "$fallback" "$home/dev/innit-sas" 'navigation fallback' +assert_navigation_fallback "set -gx BARBATDEV_DIR '$workspace/missing'; barbat" "$workspace/missing" "$workspace" 'barbat missing directory' -mkdir -p "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" "$home/dev/worktrees" +mkdir -p "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$workspace/innit-sas" "$workspace/barbatdev/products" "$home/dev/worktrees" set -l extended_navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/cowork'; set -gx PERSONAL_PROJECTS_DIR '$workspace/personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; personal; pwd; tools; pwd; worktrees; pwd; work; pwd" | string collect) set -l expected_extended_navigation (printf '%s\n' "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$home/dev/worktrees" "$workspace/cowork" | string collect) assert_equal "$extended_navigation" "$expected_extended_navigation" 'extended navigation destinations' set -l extended_fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; work; pwd; personal; pwd" | string collect) -set -l expected_extended_fallback (printf '%s\n' "$workspace/barbatdev/innit" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" | string collect) +set -l expected_extended_fallback (printf '%s\n' "$workspace" "$workspace" "$workspace/barbatdev/products" | string collect) assert_equal "$extended_fallback" "$expected_extended_fallback" 'extended navigation fallbacks' -assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; cowork" "$workspace/missing-work" 'cowork missing directories' -assert_navigation_error "set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; personal" "$workspace/missing-personal" 'personal missing directories' -assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tools" "$workspace/missing-barbat/tools" 'tools missing directory' -assert_navigation_error "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" 'worktrees missing directory' -assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" 'work missing directories' +assert_navigation_fallback "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; cowork" "$workspace/missing-work" "$workspace" 'cowork missing directories' +assert_navigation_fallback "set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; personal" "$workspace/missing-personal" "$workspace" 'personal missing directories' +assert_navigation_fallback "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tools" "$workspace/missing-barbat/tools" "$workspace" 'tools missing directory' +assert_navigation_fallback "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" "$workspace" 'worktrees missing directory' +assert_navigation_fallback "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" "$workspace" 'work missing directories' set -l aliases (run_fish -c 'functions gpl' | string collect) string match -q '*git pull*' "$aliases"; or fail 'gpl alias' run_fish -c 'functions -q gs; and exit 1; or exit 0'; or fail 'gs alias should not exist' -printf '#!/bin/sh\necho Linux\n' >"$bin/uname" -chmod +x "$bin/uname" -set -l linux_open_alias (run_fish -c 'functions -q o; and echo defined; or echo absent') -assert_equal "$linux_open_alias" absent 'Linux Fish startup excludes the macOS open alias' - -mkdir -p "$home/.local/bin" "$home/.opencode/bin" "$home/.local/share/mise/shims" "$home/.nix-profile/bin" -printf '#!/bin/sh\n' >"$home/.local/bin/w2-local-command" -chmod +x "$home/.local/bin/w2-local-command" -printf '#!/bin/sh\n' >"$home/.opencode/bin/w2-opencode-command" -chmod +x "$home/.opencode/bin/w2-opencode-command" -printf '#!/bin/sh\n' >"$home/.local/share/mise/shims/w2-mise-command" -chmod +x "$home/.local/share/mise/shims/w2-mise-command" -printf '#!/bin/sh\n' >"$home/.nix-profile/bin/w2-profile-command" -chmod +x "$home/.nix-profile/bin/w2-profile-command" -set -l linux_local_command (run_clean_fish -c "source '$xdg/fish/conf.d/10-core.fish'; command -s w2-local-command") -assert_equal "$linux_local_command" "$home/.local/bin/w2-local-command" 'Linux Fish startup resolves commands from the user-local bin directory' -set -l linux_opencode_command (run_clean_fish -c "source '$xdg/fish/conf.d/10-core.fish'; command -s w2-opencode-command") -assert_equal "$linux_opencode_command" "$home/.opencode/bin/w2-opencode-command" 'Linux Fish startup resolves commands from the OpenCode bin directory' -set -l linux_mise_command (run_clean_fish -c "source '$xdg/fish/conf.d/10-core.fish'; command -s w2-mise-command") -assert_equal "$linux_mise_command" "$home/.local/share/mise/shims/w2-mise-command" 'Linux Fish startup resolves commands from the mise shims directory' -set -l linux_profile_command (run_clean_fish -c "source '$xdg/fish/conf.d/10-core.fish'; command -s w2-profile-command") -assert_equal "$linux_profile_command" "$home/.nix-profile/bin/w2-profile-command" 'Linux Fish startup resolves commands from the user Nix profile' -set -l linux_path (run_clean_fish -c "source '$xdg/fish/conf.d/10-core.fish'; string join '|' \$PATH") -string match -q "$home/.local/bin|$home/.opencode/bin|$home/.local/share/mise/shims|$home/.nix-profile/bin|*" "$linux_path"; or fail 'Linux Fish startup orders portable tool directories before the user Nix profile' - printf '#!/bin/sh\n[ "$1" = init ] && printf "set -gx W2_STARSHIP_READY 1\\n"\n' >"$bin/starship" chmod +x "$bin/starship" set -l noninteractive (run_fish -c 'set -q W2_STARSHIP_READY; and echo ready; or echo skipped') diff --git a/fish/themes/refactoria.fish b/fish/themes/refactoria.fish new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/fish/themes/refactoria.fish @@ -0,0 +1,41 @@ +# RefactorIA Fish colors. + +set -l base "#0B0714" +set -l panel "#17102A" +set -l text "#FFFFFF" +set -l selection_text "#FFFFFF" +set -l active_violet "#A477FF" +set -l bright_lavender "#C7A6FF" +set -l bright_amber "#FFE08A" +set -l amber "#FFC857" +set -l bright_blue "#8CC8FF" +set -l error "#FF6B8A" +set -l muted "#B8AEC8" +set -l dim "#A39AB5" +set -l selection "#7127FF" +set -l search_background "#2C2144" + +# Syntax highlighting colors. +set -g fish_color_normal $text +set -g fish_color_command --bold $active_violet +set -g fish_color_keyword --bold $bright_lavender +set -g fish_color_quote $bright_amber +set -g fish_color_redirection $bright_blue +set -g fish_color_end $amber +set -g fish_color_error --bold $error +set -g fish_color_param $bright_lavender +set -g fish_color_comment $dim +set -g fish_color_selection --background=$selection $selection_text +set -g fish_color_search_match --background=$search_background $selection_text +set -g fish_color_operator $bright_blue +set -g fish_color_escape $active_violet +set -g fish_color_autosuggestion $muted +set -g fish_color_option $bright_lavender + +# Completion pager colors. +set -g fish_pager_color_progress $dim +set -g fish_pager_color_prefix --bold $active_violet +set -g fish_pager_color_completion $text +set -g fish_pager_color_description $muted +set -g fish_pager_color_selected_background --background=$selection $selection_text +set -g fish_pager_color_secondary_background --background=$panel diff --git a/herdr/config.toml b/herdr/config.toml index 06c622c..f2d0204 100644 --- a/herdr/config.toml +++ b/herdr/config.toml @@ -10,7 +10,7 @@ show_agent_labels_on_pane_borders = true prompt_new_tab_name = true pane_borders = true pane_gaps = true -accent = "#F97316" +accent = "#7127FF" [terminal] new_cwd = "follow" @@ -22,23 +22,27 @@ resume_agents_on_restore = true manage_ssh_config = false [theme] -name = "cortex" +# One Dark supplies structural defaults; every visible custom role is RefactorIA. +name = "one-dark" auto_switch = false [theme.custom] -background = "#0B0B0A" -panel_bg = "#27231D" -element_bg = "#39342C" -text = "#F8F1E7" -muted = "#B9AD9C" -subtle = "#7D7468" -accent = "#F97316" -accent_hover = "#FB923C" -blue = "#60A5FA" -red = "#F87171" -green = "#10B981" -yellow = "#FBBF24" -purple = "#A78BFA" -cyan = "#2DD4BF" -diff_added_bg = "#0D2A20" -diff_removed_bg = "#2C1210" +panel_bg = "#0B0714" +sidebar_bg = "#17102A" +active_row_bg = "#2C2144" +selection_bg = "#7127FF" +surface0 = "#17102A" +surface1 = "#2C2144" +surface_dim = "#0B0714" +overlay0 = "#EBEBEB" +overlay1 = "#F2EFFA" +text = "#FFFFFF" +subtext0 = "#EBEBEB" +accent = "#FFC857" +blue = "#8CC8FF" +red = "#FF6B8A" +green = "#58E6A8" +yellow = "#FFC857" +mauve = "#C7A6FF" +teal = "#66E6E6" +peach = "#FFC857" diff --git a/herdr/profiles/refactoria.toml b/herdr/profiles/refactoria.toml new file mode 100644 index 0000000..60da92b --- /dev/null +++ b/herdr/profiles/refactoria.toml @@ -0,0 +1,34 @@ +# RefactorIA Herdr profile. Loaded after the shared default config. + +[theme] +# One Dark supplies structural defaults; every visible custom role is RefactorIA. +name = "one-dark" +auto_switch = false + +[theme.custom] +panel_bg = "#0B0714" +sidebar_bg = "#17102A" +active_row_bg = "#2C2144" +selection_bg = "#7127FF" +surface0 = "#17102A" +surface1 = "#2C2144" +surface_dim = "#0B0714" +overlay0 = "#EBEBEB" +overlay1 = "#F2EFFA" +text = "#FFFFFF" +subtext0 = "#EBEBEB" +accent = "#FFC857" +blue = "#8CC8FF" +red = "#FF6B8A" +green = "#58E6A8" +yellow = "#FFC857" +mauve = "#C7A6FF" +teal = "#66E6E6" +peach = "#FFC857" + +[ui] +accent = "#7127FF" +show_agent_labels_on_pane_borders = true + +[ui.toast] +delivery = "herdr" diff --git a/local/env.zsh.example b/local/env.zsh.example index ce3510c..d759cc6 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -9,6 +9,7 @@ # Configuración propia de cortex-dotfiles. # export CORTEX_DOTFILES_DIR="$BARBATDEV_DIR/cortex/cortex-dotfiles" +# export CORTEX_SHELL_INTEGRATION="$CORTEX_HOME/shell/cortex.zsh" # Proyectos personales # export PERSONAL_PROJECTS_DIR="${WORKSPACE_DIR:-$HOME/dev}/local" diff --git a/nix/home.nix b/nix/home.nix index 777379d..2810e44 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -18,6 +18,8 @@ let "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/refactoria.fish".source = ../fish/functions/refactoria.fish; + "fish/functions/gentle.fish".source = ../fish/functions/gentle.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; diff --git a/odd/tasks/refactoria-reusable-cli-agent-gentle.md b/odd/tasks/refactoria-reusable-cli-agent-gentle.md new file mode 100644 index 0000000..1940179 --- /dev/null +++ b/odd/tasks/refactoria-reusable-cli-agent-gentle.md @@ -0,0 +1,28 @@ +# Reusable RefactorIA CLI visuals for agent-gentle + +## Goal and authority +The user chose a reproducible visual-only recipe first, then application on `agent-gentle`. The approved `agent-dev` presentation is the reference; each tool keeps its own palette hierarchy. Scope on this Linux/Fish host: Herdr, Pi, Claude Code (including the corrected minimal statusline), OpenCode, Starship and Fish greeting/help/theme. Zsh is absent, so do not install or configure it. Exclude Ghostty, Neovim, SketchyBar, Herdr binary patch, broader packages/services, and the broad `install.sh`. No commits, push or session restarts without a separate decision. Preserve unrelated local and host work. + +## Target discovery and safety boundary +- `agent-gentle` is Linux, login Fish, `/home/jbarbat`; reachable only using the pre-existing SSH host key under `HostKeyAlias=agent-gentle`, with strict checking and no key updates. The initial alias invocation failed because `known_hosts` pins the alias rather than the numeric HostName. No key was accepted or changed. +- No cortex-dotfiles checkout is present in bounded likely locations. All relevant existing configs are regular host files. Pi selects `omarchy-system`, OpenCode main config selects `system`, Claude has no selected theme/statusLine, Herdr has terminal/blue with `herdr 0.8.2`, Starship and Fish have older visual sources; some Fish helper commands in the approved help are unavailable here. No existing symlink is to be replaced without explicit scope review. +- Sources in the current Mac worktree and approved `agent-dev` remote remain uncommitted. Local `main` and `origin/main` advanced together to `0c0c0665`, while this dirty feature worktree remains at `72f93f9d`; `agent-dev` stays at `678d897f`. Main changed Fish core/installer helpers, not the approved RefactorIA theme sources. Do not fetch, merge, switch branches, or infer a deployment source from `main`. Reproducible here means a checked-in-ready Linux visual recipe and source assets that can be rerun from an explicitly selected source tree; a clean clone still requires separately authorized versioning. +- Before any target config write: run isolated tests and a no-write target plan, capture type/mode/hash/absence of every exact destination in a private backup manifest, prewrite recheck, use atomic replacement, and retain bounded rollback. Keep settings data private; report only targeted selectors/roles. Preserve Herdr non-theme values and unrelated JSON settings. Do not deploy if Herdr 0.8.2 rejects the candidate. + +## Tasks +- [x] **T1 — Canonical portable assets.** Repo `claude/statusline-command.sh` matches approved agent-dev SHA `8b3524b8...e454c` (five segments, Linux-safe Unicode and 80% threshold); Fish help filters unavailable helper signatures without altering layout. Independent asset tests, Bash/Fish syntax and diff check passed. Rich `claude/statusline.sh` remains untouched. No commit per user constraint. +- [x] **T2 — Replace oversized draft with a bounded Linux recipe.** User chose simplification after the general planner/backup/test draft reached 2,113 lines before apply. The first 'narrow' `scripts/refactoria_linux.py` grew to 663 lines plus 558 tests even after repairing verified backup-root, plan-drift, manifest and partial-failure defects; it failed the user's simplification goal despite 29 passing local tests. The obsolete generic draft was removed; the large narrow draft was never selected for host execution and was later removed after explicit authorization. A read-only negative-control and candidate check under temporary XDG_CONFIG_HOME proved Herdr 0.8.2 accepts the proposed theme (`config: ok`); the live config stayed untouched. The user selected a short manual backed-up playbook with explicit guided rollback instead of the larger automatic transaction engine. The large narrow draft is not a deployment asset; remove it and its tests only after the manual procedure is documented and safely rehearsed. No target config writes yet. +- [x] **T3 — Document and rehearse manual recipe.** Record the 14 guarded target writes and the separately selected Pi default, source staging, no-write inventory, private backups, per-tool guarded copies/selector merges, parser checks and guided rollback in a concise runbook. A first 360-line draft (`docs/refactoria-agent-gentle-rollout.md`) is explicitly marked UNSAFE/DO NOT RUN: it would reject the actual Fish activation only after earlier writes, widen existing settings modes, fail on absent profile/theme parent dirs, miss some symlink ancestors, and double-process rollback rows. A second shortened draft still failed independent adversarial review: staging-parent symlink escape, predictable write-temp overwrite, composite Claude `statusLine` printed, selector/candidate expectations not enforced, rollback's human gate does not pause and can follow symlinks, and unpreserved mode/owner drift. The document is now explicitly UNSAFE/DO NOT RUN design notes. The user selected a revised manual rollout **one tool at a time** after both all-in-one drafts failed review: stage/plan and private backup, isolated rehearsal, guarded apply, static verification, then stop for a check before the next tool. The unsafe all-in-one document remains DO NOT RUN and must not be used as instructions. Start with Herdr only. For its two paths, a small Herdr-specific command helper was explored, but the first implementation (`scripts/refactoria_herdr_rollout.py`, 323 lines + 190 tests) is **REJECTED / DO NOT RUN** despite 9 passing tests: independent review found a post-replace/pre-journal crash gap, non-resumable partial rollback, and concurrent drift overwrite; it also exceeds the per-tool review budget. Do not stage it on agent-gentle. Replace with a genuinely short exact-path manual backup/apply/restore procedure, rehearsed in isolation and guarded by the two pinned candidate hashes. Subsequent Pi/Claude/OpenCode/Starship/Fish batches each need their own read-only inventory, explicit allowlist, backup and checks. No target config writes before the corresponding per-tool rehearsal, no `install.sh`. Herdr read-only plan (2026-09-27): target config regular 0644 uid/gid 1000/1000 SHA `d618349e3a17b84a530e5c4b41429aae2e98d6fd06601cd59f0386965bc95ca4`, profile absent; approved source hashes `7134a7e7...a158`/`db5fb9e9...6e33`; candidate `cc60b9668a7f162516a94bbee3b23c3e3aa8bcfaebc9e90b6eea5e3e2403b945` matches prior isolated Herdr 0.8.2 check, TOML parses, 87 nonvisual lines and parsed nonvisual subtrees retained, merge idempotent. Focused 8 helper tests and an isolated fixture passed; `python3 -m unittest scripts.test_refactoria_herdr` failed only because the test imports the bare module, while canonical `python3 scripts/test_refactoria_herdr.py` passes. A generic scout gave unsafe false `/tmp`/known_hosts instructions and is not an execution source. No target config writes; backup/apply/restore still pending. + **T3 closure:** the unsafe drafts were superseded by `docs/refactoria-agent-gentle-rollout.md` (manual 71-line per-tool record); all six tool batches were independently rehearsed and backed up, checked statically, then approved visually. Earlier DO NOT RUN references above describe historical rejected revisions, not the committed guide. +- [x] **T4 — Pilot on agent-gentle.** Herdr-only phase applied on 2026-09-27: real `~/.config/herdr/config.toml` SHA changed from `d618349e…bc95ca4` to `cc60b966…03b945` (regular 0644 uid/gid1000/1000); new `~/.config/herdr/profiles/refactoria.toml` SHA `db5fb9e9…6e33` regular 0644. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/herdr-_dvfiir9/` mode0700, `config-original.toml` and `profile-absent` mode0600. Private staging `/home/jbarbat/.local/state/refactoria-staging/herdr.bu4M9Wm4/` mode0700 contains candidate, profile and a one-host `herdr-manual.py` SHA `704f0310937033076d5587179393829b28978d85f998c075e9b68f88fc843da2`; this is **not** the rejected 323-line repo helper or the unsafe all-tool recipe. The short private helper was rehearsed through backup/apply/rollback in isolated target HOME under `umask 077`, plus partial-state rollback; independently reviewed, with remaining same-user race/power-loss caveats. Prewrite backup/stage/live hashes and modes passed direct readback; `herdr config check` returned `config: ok` before and after apply; direct postwrite SHA/mode/owner/parsed nonvisual preservation checks passed. No other target config files changed; no app restart. **Human visual check of Herdr passed:** the user opened a fresh session and selected “Se ve bien.” Pi may be planned as the next separate guarded tool batch. Fresh read-only Pi inventory found unexpected selector drift: `~/.pi/agent/settings.json` regular0644 uid/gid1000 SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` now has `theme="Gentleman-Sexy"` (previously `omarchy-system`), while `~/.pi/agent/themes/refactoria.json` is absent; Pi version 0.87.1. User selected installing the RefactorIA Pi theme **without changing** `Gentleman-Sexy` default; `pi --use-theme refactoria` is the opt-in invocation. Only `~/.pi/agent/themes/refactoria.json` may be created, guarded by its absent pre-state; settings.json must remain byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900`. Pi opt-in install completed on 2026-09-27: `~/.pi/agent/themes/refactoria.json` regular0644 uid/gid1000/1000 SHA `add53fd1744a052af7630508257b0cd6cfc7e53739088370207408f5918347ee`; `settings.json` remains byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` with `theme="Gentleman-Sexy"`. Private absent-marker backup `/home/jbarbat/.local/state/refactoria-backups/pi.oZ39kbUB/` mode0700 (`theme-absent` 0600); staging `/home/jbarbat/.local/state/refactoria-staging/pi.D3r5sdH3/` private. Isolated Linux HOME rehearsal passed backup/apply/hash-guarded exact-file removal; independent live static checks passed. Opt in with `pi --use-theme refactoria`; the user ran a fresh invocation and selected “Se ve bien,” closing the Pi visual gate. Claude may be planned next. No existing Pi settings were written. If rollback is needed, exact backup and separately confirmed `rollback` phase in the private staged helper accept only baseline/candidate hashes; it intentionally retains an empty `profiles/` directory after restoring the absent profile. Claude batch applied on 2026-09-27 after separate private HOME full+partial rollback rehearsal: `~/.claude/settings.json` SHA `3c4e1bbf8ee61dfd7d957193e92683a599b6da81eddd73b84536dae8dd7338ef` regular0644 (from backup SHA `7fe09aff…2323f6`), `~/.claude/themes/refactoria.json` SHA `8f2c3994…e1020e` regular0644 and `~/.claude/statusline-command.sh` SHA `8b3524b8…e454c` regular0755. Unrelated five settings keys are deep-equal to backup; selector `custom:refactoria` and `statusLine` command match approved agent-dev setup. Private backup `/home/jbarbat/.local/state/refactoria-backups/claude.7rr9KBTd/` (0700, original settings and absent markers 0600) and stage `/home/jbarbat/.local/state/refactoria-staging/claude.zxE1uNZ7/` retained. Live Bash syntax and UTF-8/80% fixture passed, jq/Bash/Git/locale available. An independent verifier confused SHA256 with SHA3 and missed the theme file; direct parent readback confirmed all three exact paths and hashes. The user opened a fresh Claude session and selected “Se ven bien” for theme and statusline, closing the Claude visual gate; OpenCode may be planned next. OpenCode batch applied on 2026-09-27 after full and partial isolated Linux HOME rollback rehearsal: `~/.config/opencode/opencode.json` regular0644 SHA `1dbba72622cb6459b6c75906fc1d9ac352f1c7917d3c039f4ca079ae23315fd6`, `tui.json` regular0644 SHA `c450eaa8533ee5a06570dabeacba164e2f51627ebf9ba10a3863409b42bc1c21`, new `themes/refactoria.json` regular0644 SHA `612dd0a82986d8ea9f5478867aed8d6f29e525c3ee37cf2662749a0d2bd6276a`; both selectors `theme="refactoria"`, all non-theme keys deep-equal to backup. Private backup `/home/jbarbat/.local/state/refactoria-backups/opencode.qqD2OtS1/` (0700; original configs and absent marker0600), stage `/home/jbarbat/.local/state/refactoria-staging/opencode.bcxQWM2d/` retained. Direct parent postwrite hash/mode/owner/JSON checks passed, no app restart. The user opened a fresh OpenCode session and selected “Se ve bien,” closing the OpenCode visual gate; Starship may be planned next. Starship candidate source SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` passed TOML parse and Starship 1.26 isolated `print-config`/`prompt` under private stage `/home/jbarbat/.local/state/refactoria-staging/starship.VtNWyyNJ/`; current live config is regular0644 uid/gid1000 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`. Backup/apply/rollback rehearsal passed, but the verifier placed two 0700 owned fixtures under `/home/jbarbat/dev/refactoria-rehearsal-fixture{,2}` rather than requested stage. Parent direct read-only inspection found only bounded fake HOME config/backup files, no symlinks/foreign owners; **retain, do not `rm -rf`** until explicit cleanup decision. The incident was separately diagnosed and fixtures retained. After fresh strict-SSH stage/live readback, Starship was applied on 2026-09-27: `/home/jbarbat/.config/starship.toml` regular0644 uid/gid1000/1000 SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5`. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/starship.M761Rlpy/` (0700; original file0600 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`, apply-intent0600) and stage retained. Direct parent remote readback confirmed SHA/mode/owner/TOML palette; live `starship print-config` and `starship prompt` exit0. A delegated verifier mistakenly checked the Mac (`/Users/jbarbat`) instead of the explicit Linux SSH target and falsely reported the backup missing; its report was discarded. The user opened a fresh Fish shell and selected “Se ve bien” for the Starship prompt, closing that visual gate; Fish visuals may be planned next. Fish batch applied on 2026-09-27 after corrected baseline gate and isolated full+partial rollback rehearsal: new `~/.config/fish/themes/refactoria.fish` regular0644 SHA `3b04a53612cf0c0f76dea3b9e6c08f4485c24a451eac46a332ede9d00b5b9583`, `functions/fish_greeting.fish` SHA `15450023712b8c517cd1203e8de636425c97b669d6f3b9f1af403184ef904f6c`, `functions/help-profile.fish` SHA `b079bdc6110cfb55e129cd84654f93951d014b1a7d817df682df21f904a22626`, and `conf.d/zz-refactoria-theme.fish` wrapper SHA `b0cb6463e40437d772afc12e0b3577428d540528eb5ab2dafb706c8b2a086ea8` replacing old full-theme SHA **`8ebfb078e66c0c0109dbc1708a4060a7f4a93f13fa2494d41164218c89fdedfb`** (64 hex). `config.fish` remains byte-for-byte SHA `9d784d88e78620d459d01c8e385851df6f46d675f28fcfe6eb2db3c7facdfeb5`. Private backup `/home/jbarbat/.local/state/refactoria-backups/fish.aT7Q5jpk/` mode0700 (old activation and absent markers0600), stage `/home/jbarbat/.local/state/refactoria-staging/fish.PoXzl17V/` retained. Direct strict-SSH postwrite hashes/modes/owner, Fish4.9.2 `fish -n` and no-config sourced greeting/help runtime passed. The user opened a fresh Fish shell and selected “Se ven y funcionan bien” for greeting, colors and `help-profile`, closing the last visual gate. Actual target writes total 14, not 15: Pi `settings.json` was intentionally preserved as `Gentleman-Sexy`. The initial recorded activation SHA carried an erroneous 65th trailing `5`; the first fixture setup failed closed before target writes, then scripts were corrected and re-rehearsed. A concise, nonautomatic 70-line per-tool runbook now replaces the UNSAFE all-tool draft at `docs/refactoria-agent-gentle-rollout.md`; its source SHA table was checked directly. **Pi default follow-up resolved:** our Pi application preserved `settings.json` SHA `3f3b6489…b900` with `Gentleman-Sexy`, but a later read-only audit after fresh visual sessions found current SHA `e41d483e74d48a3ff02594ac4b60be09160110a65284f2880a0078cc965f0067` and `theme="refactoria"`; actor/cause unknown. The user explicitly chose **RefactorIA as the saved default** and no settings write was needed. The runbook distinguishes our 14 guarded target writes from this subsequent host setting. A later direct strict-SSH audit also found `~/.claude/settings.json` now SHA `cea2d41181e38bd439c919b64fa039d73fae6fa99c41525f94ecc1c69120787e`, rather than guarded after SHA `3c4e1bbf…38ef`; approved theme/statusLine remain exact and all five backed-up original keys deep-equal, with only new `skipDangerousModePermissionPrompt` as an additional key. The old byte-guarded rollback must refuse this drift; preserve the extra setting and use a new reviewed semantic merge if visual selectors ever need removal. An independent verifier mistakenly reported four local source files missing from a different cwd and six new Claude keys; direct parent readback corrected both. The user authorized exact-path cleanup: both misplaced `~/dev/refactoria-rehearsal-fixture{,2}` roots were revalidated and removed without touching private actual backups; an initial explicit Python removal stopped after the first fixture's files/inner dirs due incorrect directory ordering, then fresh direct inventory confirmed only two empty child dirs and an intact second fixture before the corrected deepest-first removal completed. A delegated incident report was stale and not used for mutation. Four rejected local untracked drafts (`scripts/refactoria_linux.py`, `scripts/test_refactoria_linux.py`, `scripts/refactoria_herdr_rollout.py`, `scripts/test_refactoria_herdr_rollout.py`) were SHA-guarded and removed. Other drafts/caches/dirty work remain. The user separately authorized **reviewable scoped commits without push**, pending exact-file slice planning and tests. + +## Superseded draft checkpoint +The general planner (`scripts/refactoria_visual.py` and Herdr module) and backup library passed independent isolated tests, including a corrected `[[remote]]` TOML corruption regression. They have **not** applied anything to `agent-gentle`. Keep those local draft files until a tested narrow replacement is ready, then remove only the unused draft modules/tests. The preexisting dirty `install.sh` (Aug29; SHA `86d005...c9e0`) was not touched. + +## Authorized commit slices (no push) +The user authorized scoped work-unit commits after the six visual gates, plus one ~472-line Herdr helper+tests review-budget exception. Planned order: (1) Herdr visual-only hunk of tracked `herdr/config.toml` plus portable profile, leaving unrelated onboarding/keys/status/sound unstaged; (2) Herdr merge helper plus focused tests (exception); (3) Fish theme/greeting/help; (4) Claude theme/minimal statusline plus portable asset checks (also exercises Fish help already present); (5) Pi theme; (6) OpenCode theme; (7) Starship palette; (8) Linux manual runbook and ODD evidence. Verify each index diff and focused checks before committing, record commit IDs here, and never stage unrelated dirty work. The Linux runbook uses **nine** portable replay inputs: the Herdr profile supplies identical visual merge roles without depending on the Mac `herdr/config.toml`'s unrelated local settings. Push/PR/merge remain separate decisions. + +**Commit evidence (feature branch, no push):** `ec1e4ac745b3fb63aeee681817ca184dfca3abfe` Herdr visual-only config/profile (index diff/check clean; nonvisual worktree changes remain); `c9ee5edbfd0d1f03e4a2f30b5896700819052cd4` Herdr helper+191-line focused tests, 473 added lines under the explicit exception (`python3 scripts/test_refactoria_herdr.py` PASS); `c82b2dfd9551bccae5baad2e60e0669e83a82677` Fish theme/greeting/help (`fish -n` and no-config sourced runtime PASS); `918e009631326f9fe29d47240385d28962540f28` Claude theme/statusline plus cross-surface asset test (`python3 scripts/test-refactoria-assets.py`, `bash -n`, JSON parse PASS); `fa852d98f27f5490caf1b1b0e8dafdc253bda0a0` Pi theme (JSON parse PASS); `26141c5e7a7c67d21fe5b1d1e61cb1b0f230d013` OpenCode theme (JSON parse PASS); `7310b896285bc84d202cb5d22011fdf0c623eca5` Starship palette (Starship 1.26 `print-config` PASS). Every commit staged only named paths, passed `git diff --cached --check`, and left the index empty; `61f1082238af06dd06f4e6579f01d78e0087c4d6` manual runbook/task evidence (nine committed replay SHA checks PASS). A clean `git archive` of that HEAD passed Herdr and asset tests, Fish/Bash syntax, JSON/TOML parses and Starship print-config; direct strict-SSH readback confirmed 13 byte-pinned visual files, both app selectors, six private backup roots and absence of both misplaced fixtures. The final task checkbox reconciliation follows in its own evidence-only commit. The no-config Fish runtime check and all six human visual approvals cover live behavior, but a fresh remote clean clone cannot access the branch until the user separately authorizes push. + +## Review and delivery gates +Prefer one serialized writer and independent bounded verification per source unit. Keep each reviewable unit below a reasonable authored diff; if a cohesive unit exceeds 400 authored lines, surface workload before widening scope, not code-golf. Record failed/skipped checks honestly. All four task outcomes, focused checks and eight scoped work-unit commit identities are recorded above after the user's explicit commit authorization; no push/PR/merge was authorized. Do not stage the remaining dirty work to satisfy a checkbox. The native review switch is off. Neither an on-disk selector nor static validation proves a visible theme; user observation remains the final display check. diff --git a/opencode/themes/refactoria.json b/opencode/themes/refactoria.json new file mode 100644 index 0000000..86ee4d2 --- /dev/null +++ b/opencode/themes/refactoria.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "defs": { + "bg": "#0B0714", + "panel": "#17102A", + "element": "#2C2144", + "text": "#FFFFFF", + "muted": "#B6B2C0", + "subtle": "#918C9C", + "logoViolet": "#744BED", + "activeViolet": "#9B7BFF", + "textAccent": "#A477FF", + "lavender": "#C4B5FD", + "brightLavender": "#E9D5FF", + "cyan": "#7DD3FC", + "brightBlue": "#BAE6FD", + "amber": "#FBBF24", + "brightAmber": "#FFE08A", + "red": "#F05A5A" + }, + "theme": { + "primary": "textAccent", + "secondary": "cyan", + "accent": "textAccent", + "error": "red", + "warning": "amber", + "success": "cyan", + "info": "brightBlue", + "text": "text", + "textMuted": "muted", + "background": "bg", + "backgroundPanel": "panel", + "backgroundElement": "element", + "border": "muted", + "borderActive": "activeViolet", + "borderSubtle": "subtle", + "diffAdded": "cyan", + "diffRemoved": "red", + "diffContext": "muted", + "diffHunkHeader": "brightBlue", + "diffHighlightAdded": "brightBlue", + "diffHighlightRemoved": "red", + "diffAddedBg": "#0F1D26", + "diffRemovedBg": "#2A1014", + "diffContextBg": "panel", + "diffLineNumber": "muted", + "diffAddedLineNumberBg": "#0F1D26", + "diffRemovedLineNumberBg": "#2A1014", + "markdownText": "text", + "markdownHeading": "brightLavender", + "markdownLink": "brightBlue", + "markdownLinkText": "lavender", + "markdownCode": "brightAmber", + "markdownBlockQuote": "muted", + "markdownEmph": "lavender", + "markdownStrong": "text", + "markdownHorizontalRule": "muted", + "markdownListItem": "cyan", + "markdownListEnumeration": "lavender", + "markdownImage": "brightBlue", + "markdownImageText": "lavender", + "markdownCodeBlock": "text", + "syntaxComment": "muted", + "syntaxKeyword": "brightLavender", + "syntaxFunction": "brightBlue", + "syntaxVariable": "text", + "syntaxString": "brightAmber", + "syntaxNumber": "lavender", + "syntaxType": "cyan", + "syntaxOperator": "lavender", + "syntaxPunctuation": "muted" + } +} diff --git a/pi/themes/refactoria.json b/pi/themes/refactoria.json new file mode 100644 index 0000000..92cb79f --- /dev/null +++ b/pi/themes/refactoria.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://raw.githubusercontent.com/earendil-works/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "refactoria", + "vars": { + "bg": "#0B0714", + "bgPanel": "#17102A", + "bgElement": "#2C2144", + "bgSubtle": "#17102A", + "border": "#2C2144", + "borderSubtle": "#6D627C", + "text": "#F8F8F8", + "muted": "#B6B2C0", + "mutedBright": "#D9D6E1", + "dim": "#918C9C", + "brandViolet": "#744BED", + "brandBright": "#9B7BFF", + "textAccent": "#A477FF", + "lavender": "#C4B5FD", + "brightLavender": "#E9D5FF", + "deepViolet": "#5A32A3", + "blue": "#7DD3FC", + "brightBlue": "#BAE6FD", + "warning": "#FBBF24", + "brightAmber": "#FFE08A", + "error": "#EF4444", + "selection": "#744BED", + "userBg": "#2C2144", + "userText": "#FFFFFF", + "toolPendingBg": "#121019", + "toolSuccessBg": "#0F1D26", + "toolErrorBg": "#2A1014", + "infoBg": "#17102A" + }, + "colors": { + "accent": "textAccent", + "border": "border", + "borderAccent": "brandBright", + "borderMuted": "borderSubtle", + "success": "blue", + "error": "error", + "warning": "warning", + "muted": "muted", + "dim": "dim", + "text": "text", + "thinkingText": "muted", + "selectedBg": "selection", + "scrollbarThumb": "brandViolet", + "searchMatchBg": "brandViolet", + "searchMatchText": "userText", + "userMessageBg": "userBg", + "userMessageText": "userText", + "customMessageBg": "bgSubtle", + "customMessageText": "text", + "customMessageLabel": "lavender", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "lavender", + "toolOutput": "text", + "mdHeading": "brightLavender", + "mdLink": "brightBlue", + "mdLinkUrl": "mutedBright", + "mdCode": "brightAmber", + "mdCodeBlock": "text", + "mdCodeBlockBorder": "mutedBright", + "mdQuote": "mutedBright", + "mdQuoteBorder": "lavender", + "mdHr": "muted", + "mdListBullet": "brightLavender", + "toolDiffAdded": "blue", + "toolDiffRemoved": "error", + "toolDiffContext": "muted", + "syntaxComment": "mutedBright", + "syntaxKeyword": "brightLavender", + "syntaxFunction": "brightBlue", + "syntaxVariable": "text", + "syntaxString": "brightAmber", + "syntaxNumber": "brightLavender", + "syntaxType": "brightBlue", + "syntaxOperator": "brightLavender", + "syntaxPunctuation": "mutedBright", + "thinkingOff": "dim", + "thinkingMinimal": "muted", + "thinkingLow": "brightBlue", + "thinkingMedium": "lavender", + "thinkingHigh": "warning", + "thinkingXhigh": "brandBright", + "thinkingMax": "brightLavender", + "bashMode": "warning" + }, + "export": { + "pageBg": "bg", + "cardBg": "bgElement", + "infoBg": "infoBg" + } +} diff --git a/scripts/refactoria_herdr.py b/scripts/refactoria_herdr.py new file mode 100644 index 0000000..567e414 --- /dev/null +++ b/scripts/refactoria_herdr.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +"""Herdr-specific TOML visual merge and preservation helpers.""" +from __future__ import annotations + +import json +import re +import tomllib +from typing import Any + + +class HerdrError(RuntimeError): + """A Herdr visual merge cannot be performed safely.""" + + +_HEADER = re.compile(r"^\s*\[(?!\[)(.+)\]\s*(?:#.*)?$") +_ARRAY_HEADER = re.compile(r"^\s*\[\[(.+)\]\]\s*(?:#.*)?$") +_ASSIGN = re.compile(r"^(?P[ \t]*)(?P[A-Za-z0-9_-]+|\"[^\"]+\"|'[^']+')[ \t]*=") +_VISUAL_SECTIONS = ( + ("theme", ("theme",)), + ("theme.custom", ("theme", "custom")), + ("ui", ("ui",)), +) + + +def _parse_toml(data: bytes, label: str) -> dict[str, Any]: + try: + value = tomllib.loads(data.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc: + raise HerdrError(f"malformed TOML: {label}") from exc + if not isinstance(value, dict): + raise HerdrError(f"TOML root must be a table: {label}") + return value + + +def _tables(text: str) -> tuple[dict[str, int], list[tuple[int, str, bool]]]: + headers: dict[str, int] = {} + ordered: list[tuple[int, str, bool]] = [] + for index, line in enumerate(text.splitlines()): + match = _HEADER.match(line) + is_array = False + if match is None: + match = _ARRAY_HEADER.match(line) + is_array = match is not None + if match: + name = match.group(1) + if not is_array: + if name in headers: + raise HerdrError(f"duplicate TOML table: {name}") + headers[name] = index + ordered.append((index, name, is_array)) + return headers, ordered + + +def _has(data: dict[str, Any], path: tuple[str, ...]) -> bool: + value: Any = data + for key in path: + if not isinstance(value, dict) or key not in value: + return False + value = value[key] + return True + + +def _visual_values(source: dict[str, Any]) -> dict[str, dict[str, Any]]: + theme, ui = source.get("theme"), source.get("ui") + if ( + not isinstance(theme, dict) + or not isinstance(ui, dict) + or not isinstance(theme.get("custom"), dict) + or not {"name", "auto_switch"} <= theme.keys() + or "accent" not in ui + ): + raise HerdrError("source Herdr config lacks approved visual keys") + return { + "theme": {"name": theme["name"], "auto_switch": theme["auto_switch"]}, + "theme.custom": dict(theme["custom"]), + "ui": {"accent": ui["accent"]}, + } + + +def _visual_equal(data: dict[str, Any], values: dict[str, dict[str, Any]]) -> bool: + theme, ui = data.get("theme"), data.get("ui") + custom = theme.get("custom") if isinstance(theme, dict) else None + return ( + isinstance(theme, dict) + and isinstance(custom, dict) + and isinstance(ui, dict) + and theme.get("name") == values["theme"]["name"] + and theme.get("auto_switch") == values["theme"]["auto_switch"] + and ui.get("accent") == values["ui"]["accent"] + and all(custom.get(key) == value for key, value in values["theme.custom"].items()) + ) + + +def _literal(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, str): + return json.dumps(value, ensure_ascii=False) + if isinstance(value, (int, float)): + return str(value).lower() + if isinstance(value, list): + return "[" + ", ".join(_literal(item) for item in value) + "]" + raise HerdrError("unsupported Herdr visual TOML value") + + +def _comment_start(text: str, start: int) -> int | None: + index = start + while index < len(text): + if text[index] == "#": + return index + if text.startswith('"""', index) or text.startswith("'''", index): + raise ValueError("multiline string") + if text[index] == '"': + index += 1 + while index < len(text): + if text[index] == "\\": + index += 2 + continue + if text[index] == '"': + index += 1 + break + index += 1 + else: + raise ValueError("unterminated string") + continue + if text[index] == "'": + index += 1 + while index < len(text) and text[index] != "'": + index += 1 + if index == len(text): + raise ValueError("unterminated string") + index += 1 + continue + index += 1 + return None + + +def _visual_value_span(content: str, match: re.Match[str], section: str, key: str) -> tuple[int, int]: + value_start = match.end() + while value_start < len(content) and content[value_start] in " \t": + value_start += 1 + try: + comment = _comment_start(content, value_start) + value_end = len(content) if comment is None else comment + while value_end > value_start and content[value_end - 1] in " \t": + value_end -= 1 + if value_start == value_end: + raise ValueError("missing value") + tomllib.loads("[check]\n" + content) + except (tomllib.TOMLDecodeError, ValueError) as exc: + raise HerdrError(f"unsupported multiline TOML visual key: {section}.{key}") from exc + return value_start, value_end + + +def _contains_multiline_string(text: str) -> bool: + index = 0 + while index < len(text): + if text[index] == "#": + newline = text.find("\n", index) + index = len(text) if newline < 0 else newline + 1 + continue + if text.startswith('"""', index) or text.startswith("'''", index): + return True + if text[index] == '"': + index += 1 + while index < len(text): + if text[index] == "\\": + index += 2 + continue + if text[index] == '"': + index += 1 + break + index += 1 + continue + if text[index] == "'": + index += 1 + while index < len(text) and text[index] != "'": + index += 1 + if index < len(text): + index += 1 + continue + index += 1 + return False + + +def _ensure_mergeable_tables(data: dict[str, Any], text: str) -> None: + if _contains_multiline_string(text): + raise HerdrError("unsupported multiline TOML visual key: ambiguous TOML construct") + headers, ordered = _tables(text) + visual_sections = tuple(section for section, _ in _VISUAL_SECTIONS) + for _, name, is_array in ordered: + if is_array and any( + name == section or name.startswith(section + ".") + for section in visual_sections + ): + raise HerdrError(f"unsupported visual array-of-tables: {name}") + for section, path in _VISUAL_SECTIONS: + if section not in headers and _has(data, path) and not any( + name.startswith(section + ".") for _, name, _ in ordered + ): + raise HerdrError(f"unsupported inline or dotted Herdr table: {section}") + + +def _merge_toml(text: str, values: dict[str, dict[str, Any]]) -> str: + lines = text.splitlines(keepends=True) + eol = next( + ("\r\n" if line.endswith("\r\n") else "\n" for line in lines if line.endswith(("\n", "\r\n"))), + "\n", + ) + headers, ordered = _tables(text) + additions: dict[int, list[str]] = {} + replacements: dict[int, str] = {} + + def section_end(start: int) -> int: + return next((index for index, _, _ in ordered if index > start), len(lines)) + + def first_child(name: str) -> int | None: + return next((index for index, section, _ in ordered if section.startswith(name + ".")), None) + + for section, wanted in values.items(): + start = headers.get(section) + if start is None: + insertion = first_child(section) + insertion = len(lines) if insertion is None else insertion + block = [f"[{section}]{eol}"] + block.extend(f"{key} = {_literal(value)}{eol}" for key, value in wanted.items()) + if insertion == len(lines) and lines and not lines[-1].endswith(("\n", "\r")): + block[0] = eol + block[0] + additions.setdefault(insertion, []).extend(block) + continue + + stop = section_end(start) + found: set[str] = set() + for index in range(start + 1, stop): + content = lines[index].rstrip("\r\n") + match = _ASSIGN.match(content) + key = match.group("key").strip("\"'") if match else None + if key not in wanted: + continue + value_start, value_end = _visual_value_span(content, match, section, key) + replacements[index] = ( + content[:value_start] + + _literal(wanted[key]) + + content[value_end:] + + lines[index][len(content):] + ) + found.add(key) + + missing = [key for key in wanted if key not in found] + if missing: + block = [f"{key} = {_literal(wanted[key])}{eol}" for key in missing] + if stop == len(lines) and lines and not lines[-1].endswith(("\n", "\r")): + block[0] = eol + block[0] + additions.setdefault(stop, []).extend(block) + + return "".join( + item + for index in range(len(lines) + 1) + for item in additions.get(index, []) + + ([replacements.get(index, lines[index])] if index < len(lines) else []) + ) + + +def plan_visual_merge( + source_data: bytes, + target_data: bytes, + *, + source_label: str = "herdr/config.toml", + target_label: str = ".config/herdr/config.toml", +) -> bytes: + """Return target bytes with only approved Herdr visual values merged.""" + values = _visual_values(_parse_toml(source_data, source_label)) + current = _parse_toml(target_data, target_label) + text = target_data.decode("utf-8") + _ensure_mergeable_tables(current, text) + if _visual_equal(current, values): + return target_data + + planned = _merge_toml(text, values).encode("utf-8") + if not _visual_equal(_parse_toml(planned, target_label), values): + raise HerdrError("Herdr visual merge did not produce the requested values") + return planned diff --git a/scripts/test-refactoria-assets.py b/scripts/test-refactoria-assets.py new file mode 100644 index 0000000..c1249b4 --- /dev/null +++ b/scripts/test-refactoria-assets.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Focused checks for the portable RefactorIA CLI assets.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +STATUSLINE = ROOT / "claude/statusline-command.sh" +HELP_PROFILE = ROOT / "fish/functions/help-profile.fish" +EXPECTED_STATUSLINE_SHA = "8b3524b8f1c9279f19986dbef7be0e3230d9044868d6f21b44b5ad85c40e454c" +ANSI_SGR = re.compile(r"\x1b\[[0-9;]*m") +ACCENT = "\033[1;38;2;255;107;138m" +BOLD = "\033[1m" + + +class TestFailure(RuntimeError): + """Raised when a focused asset check fails.""" + + +def require_tool(name: str) -> str: + path = shutil.which(name) + if path is None: + raise TestFailure(f"required command not found: {name}") + return path + + +def run_checked(command: list[str], *, env: dict[str, str], input_text: str = "") -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + input=input_text, + capture_output=True, + check=False, + cwd=ROOT, + env=env, + text=True, + encoding="utf-8", + ) + if result.returncode != 0: + raise TestFailure( + f"command failed ({result.returncode}): {' '.join(command)}\n" + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) + return result + + +def statusline_payload(workspace: Path, used_percentage: int | None) -> str: + context_window: dict[str, int] = {} + if used_percentage is not None: + context_window["used_percentage"] = used_percentage + return json.dumps( + { + "workspace": {"current_dir": str(workspace)}, + "context_window": context_window, + "rate_limits": {"five_hour": {"used_percentage": 42}}, + "model": {"display_name": "claude-test"}, + "effort": {"level": "high"}, + } + ) + + +def run_statusline(bash: str, workspace: Path, used_percentage: int | None, env: dict[str, str]) -> str: + result = run_checked( + [bash, str(STATUSLINE)], + env=env, + input_text=statusline_payload(workspace, used_percentage), + ) + return result.stdout + + +def strip_ansi(value: str) -> str: + return ANSI_SGR.sub("", value) + + +def assert_statusline_case( + bash: str, + workspace: Path, + used_percentage: int | None, + env: dict[str, str], +) -> None: + output = run_statusline(bash, workspace, used_percentage, env) + clean = strip_ansi(output) + segments = clean.split(" · ") + if len(segments) != 5: + raise TestFailure(f"expected five statusline segments, got {len(segments)}: {clean!r}") + + if used_percentage is None: + expected_bar = "[░░░░░░░░] ctx --" + expected_filled = 0 + if ACCENT in output or BOLD in output: + raise TestFailure("missing context unexpectedly used a threshold color") + else: + expected_filled = (used_percentage * 8 + 50) // 100 + expected_bar = ( + "[" + + "█" * expected_filled + + "░" * (8 - expected_filled) + + f"] {used_percentage}% ctx" + ) + expected_color = ACCENT if used_percentage >= 80 else BOLD + if expected_color not in output: + raise TestFailure(f"missing expected ANSI threshold color at {used_percentage}%") + unexpected_color = BOLD if expected_color == ACCENT else ACCENT + if unexpected_color in output: + raise TestFailure(f"unexpected ANSI threshold color at {used_percentage}%") + + if segments != [expected_bar, "claude-test (high)", "workspace", "main*", "5h 42%"]: + raise TestFailure(f"unexpected statusline segments at {used_percentage!r}: {segments!r}") + if output.count("█") != expected_filled: + raise TestFailure(f"expected {expected_filled} filled glyphs at {used_percentage!r}") + if output.count("░") != 8 - expected_filled: + raise TestFailure(f"expected {8 - expected_filled} empty glyphs at {used_percentage!r}") + + +def test_statusline() -> None: + if not STATUSLINE.is_file(): + raise TestFailure(f"missing statusline source: {STATUSLINE}") + actual_sha = hashlib.sha256(STATUSLINE.read_bytes()).hexdigest() + if actual_sha != EXPECTED_STATUSLINE_SHA: + raise TestFailure(f"statusline SHA mismatch: expected {EXPECTED_STATUSLINE_SHA}, got {actual_sha}") + source = STATUSLINE.read_text(encoding="utf-8") + for forbidden in ("/Users/", "/home/", "SECRET", "TOKEN"): + if forbidden in source: + raise TestFailure(f"statusline source contains forbidden host/secret marker: {forbidden}") + + bash = require_tool("bash") + require_tool("git") + require_tool("jq") + with tempfile.TemporaryDirectory(prefix="refactoria-statusline-") as temporary_directory: + temporary_root = Path(temporary_directory) + home = temporary_root / "home" + workspace = temporary_root / "workspace" + home.mkdir() + workspace.mkdir() + + env = os.environ.copy() + env.update( + { + "HOME": str(home), + "LC_ALL": "C.UTF-8", + "LANG": "C.UTF-8", + "TERM": "dumb", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": str(home / "gitconfig"), + } + ) + run_checked([require_tool("git"), "init", "-q", "-b", "main", str(workspace)], env=env) + (workspace / "dirty.txt").write_text("dirty\n", encoding="utf-8") + + for used_percentage in (None, 0, 25, 79, 80, 95, 100): + assert_statusline_case(bash, workspace, used_percentage, env) + + +def command_line_present(lines: list[str], command: str) -> bool: + return any(line.strip().startswith(command) for line in lines) + + +def test_dynamic_help_filter() -> None: + fish = shutil.which("fish") + if fish is None: + print("SKIP dynamic Fish help filter: fish binary not found") + return + + with tempfile.TemporaryDirectory(prefix="refactoria-help-") as temporary_directory: + temporary_root = Path(temporary_directory) + home = temporary_root / "home" + bin_directory = temporary_root / "bin" + home.mkdir() + bin_directory.mkdir() + for command in ("git-workdev", "dev", "cc"): + executable = bin_directory / command + executable.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable.chmod(0o755) + + env = os.environ.copy() + env.update( + { + "HOME": str(home), + "PATH": str(bin_directory), + "COLUMNS": "80", + "TERM": "dumb", + "LC_ALL": "C.UTF-8", + "LANG": "C.UTF-8", + } + ) + result = run_checked( + [ + fish, + "--no-config", + "-c", + 'source "$argv[1]"; help-profile', + str(HELP_PROFILE), + ], + env=env, + ) + lines = strip_ansi(result.stdout).splitlines() + for command in ("git-workdev", "dev", "cc [path]"): + if not command_line_present(lines, command): + raise TestFailure(f"available helper missing from Fish help: {command}") + for command in ("git-personaldev", "clone-workdev", "barbat", "oc [path]", "herdr-orient"): + if command_line_present(lines, command): + raise TestFailure(f"unavailable helper was displayed in Fish help: {command}") + + +def main() -> None: + test_statusline() + test_dynamic_help_filter() + print("PASS RefactorIA asset checks") + + +if __name__ == "__main__": + try: + main() + except TestFailure as error: + raise SystemExit(f"FAIL: {error}") from error diff --git a/scripts/test_refactoria_herdr.py b/scripts/test_refactoria_herdr.py new file mode 100644 index 0000000..a571f5d --- /dev/null +++ b/scripts/test_refactoria_herdr.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Focused tests for the Herdr TOML visual merge slice.""" +from __future__ import annotations + +import tomllib +from pathlib import Path + +import refactoria_herdr as herdr + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = (ROOT / "herdr/config.toml").read_bytes() +PROFILE = (ROOT / "herdr/profiles/refactoria.toml").read_bytes() +COMPLEX_TARGET = b"""# Preserve this non-theme comment and every unrelated setting. +onboarding = true + +[keys] +prefix = "keep-key" + +[remote] +endpoint = "keep-remote" +roles = ["remote-only"] + +[ui.toast] +delivery = "terminal" + +[ui] +sidebar_width = 37 +accent = "#000000" + +[theme] +name = "old-theme" +auto_switch = true + +[theme.custom] +panel_bg = "old-panel" +remote_only = "keep-role" +""" + +ARRAY_TARGET = b"""[theme] +name = "terminal" # keep comment +auto_switch = false +[[remote]] +name = "unrelated" +[theme.custom] +panel_bg = "black" +[ui] +accent = "blue" +""" + +SPACED_COMMENT_TARGET = b"""[theme] + name = "terminal#literal" # keep name +auto_switch\t=\tfalse\t# keep switch +[theme.custom] +panel_bg = "#000000#literal" # keep panel +[ui] +accent\t=\t"#000000#literal"\t# keep accent +""" + +CUSTOM_ARRAY_TARGET = b"""[theme] +name = "terminal" +auto_switch = false +[theme.custom] +panel_bg = "black" +[[remote]] +name = "unrelated" +[ui] +accent = "blue" +""" + + +def expect_error(source: bytes, target: bytes, text: str) -> None: + try: + herdr.plan_visual_merge(source, target) + except herdr.HerdrError as exc: + assert text in str(exc), str(exc) + else: + raise AssertionError(f"expected HerdrError containing {text!r}") + + +def test_portable_profile_matches_mac_visual_roles() -> None: + assert herdr.plan_visual_merge( + PROFILE, COMPLEX_TARGET, source_label="herdr/profiles/refactoria.toml" + ) == herdr.plan_visual_merge(SOURCE, COMPLEX_TARGET) + + +def test_complex_target_preservation_and_idempotency() -> None: + planned = herdr.plan_visual_merge(SOURCE, COMPLEX_TARGET) + parsed = tomllib.loads(planned.decode("utf-8")) + source = tomllib.loads(SOURCE.decode("utf-8")) + + assert parsed["onboarding"] is True + assert parsed["keys"]["prefix"] == "keep-key" + assert parsed["remote"] == {"endpoint": "keep-remote", "roles": ["remote-only"]} + assert parsed["ui"]["toast"]["delivery"] == "terminal" + assert parsed["ui"]["sidebar_width"] == 37 + assert parsed["theme"]["custom"]["remote_only"] == "keep-role" + assert parsed["theme"]["name"] == source["theme"]["name"] + assert parsed["theme"]["auto_switch"] == source["theme"]["auto_switch"] + assert parsed["theme"]["custom"]["panel_bg"] == source["theme"]["custom"]["panel_bg"] + assert b"# Preserve this non-theme comment" in planned + assert b'endpoint = "keep-remote"' in planned + assert b'delivery = "terminal"' in planned + + assert herdr.plan_visual_merge(SOURCE, planned) == planned + + +def test_array_headers_boundaries_and_comments_are_preserved() -> None: + planned = herdr.plan_visual_merge(SOURCE, ARRAY_TARGET) + parsed = tomllib.loads(planned.decode("utf-8")) + + assert parsed["remote"] == [{"name": "unrelated"}] + assert b'[[remote]]\nname = "unrelated"\n' in planned + assert b'name = "one-dark" # keep comment\n' in planned + + +def test_visual_value_spacing_and_quoted_hashes_are_preserved() -> None: + planned = herdr.plan_visual_merge(SOURCE, SPACED_COMMENT_TARGET) + + assert b' name = "one-dark" # keep name\n' in planned + assert b"auto_switch\t=\tfalse\t# keep switch\n" in planned + assert b'panel_bg = "#0B0714" # keep panel\n' in planned + assert b'accent\t=\t"#7127FF"\t# keep accent\n' in planned + + +def test_missing_custom_roles_are_inserted_before_following_array() -> None: + planned = herdr.plan_visual_merge(SOURCE, CUSTOM_ARRAY_TARGET) + parsed = tomllib.loads(planned.decode("utf-8")) + array_start = planned.index(b"[[remote]]") + + for key in tomllib.loads(SOURCE.decode("utf-8"))["theme"]["custom"]: + assert planned.index(f"{key} = ".encode("utf-8")) < array_start + assert parsed["remote"] == [{"name": "unrelated"}] + + +def test_visual_array_tables_and_multiline_values_fail_closed() -> None: + for target in ( + b'[[theme]]\nname = "old-theme"\nauto_switch = true\n', + b'[theme]\nname = "old-theme"\nauto_switch = true\n[[theme.custom]]\npanel_bg = "old"\n', + b'[theme]\nname = "old-theme"\nauto_switch = true\n[theme.custom]\npanel_bg = "old"\n[[ui]]\naccent = "#000"\n', + ): + expect_error(SOURCE, target, "unsupported visual array-of-tables") + + expect_error( + SOURCE, + b'[theme]\nname = """old-theme\ncontinued"""\nauto_switch = true\n', + "unsupported multiline TOML visual key", + ) + + +def test_malformed_inline_and_dotted_inputs_fail_closed() -> None: + expect_error(b"[theme\n", COMPLEX_TARGET, "malformed TOML: herdr/config.toml") + expect_error(SOURCE, b"[theme\n", "malformed TOML: .config/herdr/config.toml") + expect_error( + SOURCE, + b'theme = {name = "old-theme", auto_switch = true, custom = {panel_bg = "old"}}\nui = {accent = "#000000"}\n', + "unsupported inline or dotted Herdr table: theme", + ) + expect_error( + SOURCE, + b'theme.name = "old-theme"\ntheme.auto_switch = true\ntheme.custom.panel_bg = "old"\nui.accent = "#000000"\n', + "unsupported inline or dotted Herdr table: theme", + ) + expect_error( + SOURCE, + b'[theme]\nname = "old-theme"\nauto_switch = true\ncustom.panel_bg = "old"\n[ui]\naccent = "#000000"\n', + "unsupported inline or dotted Herdr table: theme.custom", + ) + + +def test_source_visual_keys_are_required() -> None: + expect_error( + b'[theme]\nname = "only-name"\n[theme.custom]\npanel_bg = "#000"\n', + COMPLEX_TARGET, + "source Herdr config lacks approved visual keys", + ) + + +def main() -> None: + test_portable_profile_matches_mac_visual_roles() + test_complex_target_preservation_and_idempotency() + test_array_headers_boundaries_and_comments_are_preserved() + test_visual_value_spacing_and_quoted_hashes_are_preserved() + test_missing_custom_roles_are_inserted_before_following_array() + test_visual_array_tables_and_multiline_values_fail_closed() + test_malformed_inline_and_dotted_inputs_fail_closed() + test_source_visual_keys_are_required() + print("PASS RefactorIA Herdr merge tests") + + +if __name__ == "__main__": + main() diff --git a/starship/starship.toml b/starship/starship.toml index 6bfa982..d098b20 100644 --- a/starship/starship.toml +++ b/starship/starship.toml @@ -1,12 +1,12 @@ # Formato general format = """ ${custom.repo}\ -[|](fg:color_orange)\ +[|](fg:color_brand)\ $directory\ -[|](fg:color_yellow)\ +[|](fg:color_brand)\ $git_branch\ $git_status\ -[|](fg:color_aqua)\ +[|](fg:color_brand)\ ${custom.sdd_auto}\ ${custom.sdd_interactive}\ $nodejs\ @@ -17,23 +17,24 @@ $line_break$character""" # Módulos alineados a la derecha right_format = """$cmd_duration$time""" -palette = 'innit_tech_slate' - -[palettes.innit_tech_slate] -color_fg0 = '#F6F8FA' -color_bg1 = '#1C2128' -color_bg3 = '#2D333B' -color_blue = '#0054ff' -color_aqua = '#58A6FF' -color_green = '#3FB950' -color_orange = '#0054ff' -color_purple = '#8B5CF6' -color_red = '#F85149' -color_yellow = '#D29922' +palette = 'refactoria' + +[palettes.refactoria] +color_fg0 = '#FFFFFF' +color_base = '#0B0714' +color_surface = '#17102A' +color_panel = '#2C2144' +color_brand = '#7127FF' +color_accent = '#A477FF' +color_success = '#58E6A8' +color_info = '#8CC8FF' +color_warning = '#FFC857' +color_beard = '#FFDE70' +color_error = '#FF6B8A' [os] disabled = true -style = "bg:color_orange fg:color_fg0" +style = "bg:color_surface fg:color_fg0" [os.symbols] Macos = "macOS" @@ -43,12 +44,12 @@ Linux = "Linux" [username] show_always = false -style_user = "bg:color_orange fg:color_fg0" -style_root = "bg:color_orange fg:color_fg0" +style_user = "bg:color_surface fg:color_fg0" +style_root = "bg:color_surface fg:color_fg0" format = '[ $user ]($style)' [directory] -style = "fg:color_fg0 bg:color_yellow" +style = "fg:color_fg0 bg:color_panel" format = "[ $path ]($style)" truncation_length = 3 truncation_symbol = "…/" @@ -63,14 +64,14 @@ truncation_symbol = "…/" [git_branch] symbol = "" -style = "bg:color_aqua" -format = '[[ $branch ](fg:color_fg0 bg:color_aqua)]($style)' +style = "bg:color_info" +format = '[[ $branch ](fg:color_base bg:color_info)]($style)' truncation_length = 24 truncation_symbol = "…" [git_status] -style = "bg:color_aqua" -format = '[[($all_status$ahead_behind )](fg:color_fg0 bg:color_aqua)]($style)' +style = "bg:color_info" +format = '[[($all_status$ahead_behind )](fg:color_base bg:color_info)]($style)' ahead = '↑${count}' behind = '↓${count}' diverged = '⇅↑${ahead_count}↓${behind_count}' @@ -87,7 +88,7 @@ description = "Nombre corto del repo actual" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1" shell = ["bash", "--noprofile", "--norc"] command = "basename \"$(git rev-parse --show-toplevel 2>/dev/null)\" | cut -c1-24" -style = "fg:color_fg0 bg:color_orange" +style = "fg:color_fg0 bg:color_brand" format = '[ $output ]($style)' [custom.sdd_auto] @@ -95,7 +96,7 @@ description = "Estado SDD auto del proyecto" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && test -f .ai/sdd-session.json && (jq -r '.execution_mode // empty' .ai/sdd-session.json 2>/dev/null || python3 -c \"import json; print(json.load(open('.ai/sdd-session.json')).get('execution_mode',''))\" 2>/dev/null) | grep -qx 'auto'" shell = ["bash", "--noprofile", "--norc"] command = "printf 'sdd:auto'" -style = "fg:color_fg0 bg:color_green" +style = "fg:color_success bg:color_surface" format = '[ $output ]($style)' [custom.sdd_interactive] @@ -103,20 +104,20 @@ description = "Estado SDD interactivo del proyecto" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && test -f .ai/sdd-session.json && (jq -r '.execution_mode // empty' .ai/sdd-session.json 2>/dev/null || python3 -c \"import json; print(json.load(open('.ai/sdd-session.json')).get('execution_mode',''))\" 2>/dev/null) | grep -qx 'interactive'" shell = ["bash", "--noprofile", "--norc"] command = "printf 'sdd:interactive'" -style = "fg:color_fg0 bg:color_purple" +style = "fg:color_accent bg:color_surface" format = '[ $output ]($style)' [nodejs] symbol = "" -style = "bg:color_blue" -format = '[[ node($version) ](fg:color_fg0 bg:color_blue)]($style)' +style = "bg:color_panel" +format = '[[ node($version) ](fg:color_info bg:color_panel)]($style)' detect_files = ["package.json", ".node-version", ".nvmrc"] detect_folders = ["node_modules"] [python] symbol = "" -style = "bg:color_blue" -format = '[[ py($version) ](fg:color_fg0 bg:color_blue)]($style)' +style = "bg:color_panel" +format = '[[ py($version) ](fg:color_info bg:color_panel)]($style)' detect_files = ["pyproject.toml", "requirements.txt", "requirements-dev.txt", ".python-version", "uv.lock", "Pipfile"] detect_folders = [".venv", "venv"] @@ -125,20 +126,20 @@ disabled = true [status] disabled = false -style = "fg:color_red" +style = "fg:color_error bg:color_base" symbol = 'x' format = '[ $symbol$status ]($style)' [cmd_duration] min_time = 500 -style = "bg:color_bg1" -format = '[[ $duration ](fg:color_yellow bg:color_bg1)]($style)' +style = "bg:color_surface" +format = '[[ $duration ](fg:color_warning bg:color_surface)]($style)' [time] disabled = false time_format = "%R" -style = "bg:color_bg1" -format = '[[ $time ](fg:color_fg0 bg:color_bg1)]($style)' +style = "bg:color_surface" +format = '[[ $time ](fg:color_fg0 bg:color_surface)]($style)' [battery] full_symbol = '󰁹' @@ -148,20 +149,20 @@ format = '[$symbol$percentage ]($style)' [[battery.display]] threshold = 20 -style = "bold red" +style = "bold color_error" [[battery.display]] threshold = 50 -style = "bold yellow" +style = "bold color_warning" [line_break] disabled = false [character] disabled = false -success_symbol = '[󰼀](fg:color_fg0) ' -error_symbol = '[󰼀](fg:color_red) ' -vimcmd_symbol = '[󰼀](fg:color_fg0) ' -vimcmd_replace_one_symbol = '[󰼀](fg:color_purple) ' -vimcmd_replace_symbol = '[󰼀](fg:color_purple) ' -vimcmd_visual_symbol = '[󰼀](fg:color_yellow) ' +success_symbol = '[󰼀](fg:color_beard) ' +error_symbol = '[󰼀](fg:color_error) ' +vimcmd_symbol = '[󰼀](fg:color_info) ' +vimcmd_replace_one_symbol = '[󰼀](fg:color_accent) ' +vimcmd_replace_symbol = '[󰼀](fg:color_accent) ' +vimcmd_visual_symbol = '[󰼀](fg:color_warning) ' diff --git a/zsh/cortex-dotfiles.zsh b/zsh/cortex-dotfiles.zsh index 413b9c6..c29910e 100644 --- a/zsh/cortex-dotfiles.zsh +++ b/zsh/cortex-dotfiles.zsh @@ -82,19 +82,30 @@ _go_dev_dir() { if [[ -d "$target" ]]; then cd "$target" else - echo "❌ Directorio no encontrado: $target" - return 1 + _go_first_existing_dir "$target" fi } _go_first_existing_dir() { - local target + local target probe for target in "$@"; do if [[ -d "$target" ]]; then cd "$target" return fi done + # No candidate exists: walk up to the nearest existing ancestor of the + # first target so navigation commands degrade gracefully on hosts that + # do not have this part of the workspace layout. + probe="$1" + while [[ -n "$probe" && ! -d "$probe" ]]; do + probe="${probe:h}" + done + if [[ -n "$probe" && -d "$probe" ]]; then + echo "⚠️ Directorio no encontrado: $1 — usando el ancestro más cercano: $probe" + cd "$probe" + return + fi echo "❌ Directorio no encontrado: $1" return 1 } @@ -107,6 +118,8 @@ tools() { _go_dev_dir "$TOOLS_DIR"; } worktrees() { _go_dev_dir "$WORKTREES_DIR"; } work() { _go_dev_dir "$WORK_PROJECTS_DIR"; } innit() { _go_dev_dir "$INNIT_DIR"; } +refactoria() { _go_dev_dir "${REFACTORIA_DIR:-$WORKSPACE_DIR/refactoria}"; } +gentle() { _go_dev_dir "${GENTLE_DIR:-$WORKSPACE_DIR/gentle}"; } innit-apis() { _go_dev_dir "$INNIT_APIS_DIR"; } innit-mobile(){ _go_dev_dir "$INNIT_MOBILE_DIR"; } innit-webs() { _go_dev_dir "$INNIT_WEBS_DIR"; } @@ -165,7 +178,7 @@ myip() { } # Mostrar el logo de RefactorIA en Braille Unicode bajo demanda. -refactoria() { +refactoria-logo() { local logo="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}/assets/refactoria-braille.txt" if [[ -f "$logo" ]]; then cat "$logo" @@ -249,6 +262,8 @@ export PERSONAL_PROJECTS_DIR="${PERSONAL_PROJECTS_DIR:-$WORKSPACE_DIR/local}" export TOOLS_DIR="${TOOLS_DIR:-$PERSONAL_PROJECTS_DIR/tools}" export WORKTREES_DIR="${WORKTREES_DIR:-$WORKSPACE_DIR/worktrees}" export INNIT_DIR="${INNIT_DIR:-$WORK_PROJECTS_DIR}" +export REFACTORIA_DIR="${REFACTORIA_DIR:-$WORKSPACE_DIR/refactoria}" +export GENTLE_DIR="${GENTLE_DIR:-$WORKSPACE_DIR/gentle}" export INNIT_APIS_DIR="${INNIT_APIS_DIR:-$INNIT_DIR/apis}" export INNIT_MOBILE_DIR="${INNIT_MOBILE_DIR:-$INNIT_DIR/mobile}" export INNIT_WEBS_DIR="${INNIT_WEBS_DIR:-$INNIT_DIR/webs}"