diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3283faf..9d624d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Check navigation shortcuts run: ./scripts/check-navigation-shortcuts.sh + - name: Check installer symlink safety + run: ./scripts/check-install-symlinks.sh + - name: Validate TOML configs shell: bash run: | diff --git a/README.md b/README.md index b2ffd7f..e8a9d2e 100644 --- a/README.md +++ b/README.md @@ -95,23 +95,11 @@ dotfiles/ ## Boundary con Cortex -Este repo sigue siendo standalone: `install.sh` no requiere tener el repo `cortex` disponible y estos dotfiles deben poder instalarse por sí solos. El bootstrap carga primero `~/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh` y, si existe, carga al final `~/.cortex/shell/cortex.zsh`, que pertenece a Cortex. Ambos fragments son opcionales y un error en uno no impide intentar cargar el otro. Cortex conserva el ownership de sus variables core; este repo solo define variables bajo el namespace `CORTEX_DOTFILES_*`. +Este repo sigue siendo standalone: `install.sh` no requiere tener Cortex instalado. Administra el entrypoint fijo `~/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh`, sus archivos enlazados y las variables `CORTEX_DOTFILES_*` documentadas acá. -Algunos artefactos viven acá temporalmente porque nacieron junto al setup personal, pero conceptualmente son propios del producto Cortex y no deberían tener a `cortex-dotfiles` como source of truth permanente: +El bootstrap carga dotfiles primero y después intenta cargar `~/.cortex/shell/cortex.zsh`. Esa segunda ruta y las variables core que exponga pertenecen a Cortex; este repo solo las consume. Ambos fragments son opcionales y un error en uno no impide intentar cargar el otro. -- `opencode/themes/` — themes Cortex para OpenCode. -- `claude/themes/` — themes Cortex para Claude Code. -- `claude/statusline.sh` — statusline orientada a superficies Cortex. -- `docs/agent-state-v1.md` — contrato `cortex.agent_state.v1`. -- `scripts/check-agent-state.sh` — smoke check del contrato agent-state. -- `zsh/scripts/agent-state.sh` — bridge local para reportar estado de agentes. -- `zsh/scripts/postcompact-hook.sh` y `zsh/scripts/memsave-nudge.sh` — hooks ligados al workflow Cortex. - -La migración se coordina en tres repos independientes: `cortex` define ownership de los artefactos product-owned, `cortex-dotfiles` conserva instalación/adaptación local, y `cortex-dots` sigue siendo un snapshot OSS-safe de dotfiles sin depender de `cortex`. - -No borrar ni cambiar estos artefactos acá hasta que `cortex` tenga reemplazos validados y se decida qué queda como adaptación local. - -Seguimiento: [cortex-dotfiles #31](https://github.com/barbatdev/cortex-dotfiles/issues/31), [cortex #1038](https://github.com/barbatdev/cortex/issues/1038), [cortex-dots #14](https://github.com/barbatdev/cortex-dots/issues/14). +La coordinación vigente del contrato shell se sigue en [cortex #1259](https://github.com/barbatdev/cortex/issues/1259). Las migraciones de artefactos actualmente versionados en este repo se tratan por separado en [cortex-dotfiles #31](https://github.com/barbatdev/cortex-dotfiles/issues/31). ## Especificaciones @@ -150,7 +138,6 @@ Editá `local/env.zsh` (gitignored) para configurar: - `PERSONAL_PROJECTS_DIR` — repos locales, por defecto `$WORKSPACE_DIR/local` - `TOOLS_DIR` y `WORKTREES_DIR` — herramientas locales y worktrees - `CORTEX_DOTFILES_DIR` — repo fuente de dotfiles, por defecto `$BARBATDEV_DIR/cortex/cortex-dotfiles` -- `CORTEX_DOTFILES_SHELL_ENTRYPOINT` — override del entrypoint propio, por defecto `~/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh` - `CORTEX_DOTFILES_MULTIPLEXER` — fallback standalone para helpers, por defecto `cmux`; `CORTEX_MULTIPLEXER` de Cortex tiene precedencia - `OPENCODE_DEFAULT_FLAGS` — flags por defecto para `oc` - `INNIT_DIR` y overrides `INNIT_*_DIR` — raíz y subdirectorios `apis`, `mobile`, `webs` y `pcsoft` diff --git a/install.sh b/install.sh index 4f840be..dd55433 100755 --- a/install.sh +++ b/install.sh @@ -7,6 +7,9 @@ TIMESTAMP=$(date +%Y%m%d_%H%M%S) DRY_RUN=false PLATFORM="$(uname -s)" +# shellcheck source=scripts/install-helpers.sh +source "$DOTFILES/scripts/install-helpers.sh" + if [[ "${1:-}" == "--dry-run" ]]; then DRY_RUN=true fi @@ -209,7 +212,6 @@ PY check_symlink_target "$DOTFILES/bun/bunfig.toml" "$HOME/.bunfig.toml" check_symlink_target "$DOTFILES/uv/uv.toml" "$HOME/.config/uv/uv.toml" check_symlink_target "$DOTFILES/starship/starship.toml" "$HOME/.config/starship.toml" - check_symlink_target "$DOTFILES/herdr/config.toml" "$HOME/.config/herdr/config.toml" check_symlink_target "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" check_symlink_target "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" check_symlink_target "$DOTFILES/herdr/config.toml" "$HOME/.config/herdr/config.toml" @@ -426,43 +428,12 @@ fi echo "" echo "💾 Haciendo backup de configs existentes..." -backup_if_exists() { - local src="$1" - if [[ -e "$src" && ! -L "$src" ]]; then - local backup="${src}.bak_${TIMESTAMP}" - if [[ "$DRY_RUN" == true ]]; then - echo " → Would backup $src to $backup" - else - cp -R "$src" "$backup" - echo " → Backup: $backup" - fi - fi -} - -backup_karabiner_if_exists() { - local src="$HOME/.config/karabiner/karabiner.json" - if [[ -e "$src" || -L "$src" ]]; then - local backup="${src}.bak_${TIMESTAMP}" - if [[ "$DRY_RUN" == true ]]; then - echo " → Would backup $src to $backup" - else - if [[ -L "$src" ]]; then - cp -P "$src" "$backup" - else - cp "$src" "$backup" - fi - echo " → Backup: $backup" - fi - fi -} - backup_if_exists "$HOME/.zshrc" backup_if_exists "$HOME/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh" backup_if_exists "$HOME/.npmrc" backup_if_exists "$HOME/.bunfig.toml" backup_if_exists "$HOME/.config/uv/uv.toml" backup_if_exists "$HOME/.config/starship.toml" -backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.config/ghostty/config" backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.config/opencode/tui.json" @@ -478,34 +449,19 @@ if [[ "$PLATFORM" == "Darwin" ]]; then backup_if_exists "$HOME/.yabairc" backup_if_exists "$HOME/.config/skhd/skhdrc" backup_if_exists "$HOME/.skhdrc" - backup_karabiner_if_exists + backup_if_exists "$HOME/.config/karabiner/karabiner.json" fi # --- Crear symlinks --- echo "" echo "🔗 Creando symlinks..." -create_symlink() { - local src="$1" - local dst="$2" - if [[ "$DRY_RUN" == true ]]; then - echo " → Would symlink $dst → $src" - else - mkdir -p "$(dirname "$dst")" - # -n evita que ln dereferencie un symlink-a-directorio existente y cree - # un link adentro (caso ghostty/shaders → loop shaders/shaders) - ln -sfn "$src" "$dst" - echo " ✓ $dst → $src" - fi -} - create_symlink "$DOTFILES/zsh/zshrc" "$HOME/.zshrc" create_symlink "$DOTFILES/zsh/cortex-dotfiles.zsh" "$HOME/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh" create_symlink "$DOTFILES/npm/npmrc" "$HOME/.npmrc" create_symlink "$DOTFILES/bun/bunfig.toml" "$HOME/.bunfig.toml" create_symlink "$DOTFILES/uv/uv.toml" "$HOME/.config/uv/uv.toml" create_symlink "$DOTFILES/starship/starship.toml" "$HOME/.config/starship.toml" -create_symlink "$DOTFILES/herdr/config.toml" "$HOME/.config/herdr/config.toml" create_symlink "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" create_symlink "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" create_symlink "$DOTFILES/herdr/config.toml" "$HOME/.config/herdr/config.toml" @@ -628,10 +584,15 @@ if [[ ! -f "$DOTFILES/local/env.zsh" ]]; then if [[ "$DRY_RUN" == true ]]; then echo "📝 Would create local/env.zsh from local/env.zsh.example" else - cp "$DOTFILES/local/env.zsh.example" "$DOTFILES/local/env.zsh" + install -m 600 "$DOTFILES/local/env.zsh.example" "$DOTFILES/local/env.zsh" echo "📝 Creado local/env.zsh desde el ejemplo — editalo con tus paths" fi else + if [[ "$DRY_RUN" == true ]]; then + echo " → Would set local/env.zsh permissions to 600" + else + chmod 600 "$DOTFILES/local/env.zsh" + fi echo "✓ local/env.zsh ya existe" fi diff --git a/local/env.zsh.example b/local/env.zsh.example index a8ac869..ce3510c 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -9,7 +9,6 @@ # Configuración propia de cortex-dotfiles. # export CORTEX_DOTFILES_DIR="$BARBATDEV_DIR/cortex/cortex-dotfiles" -# export CORTEX_DOTFILES_SHELL_ENTRYPOINT="$HOME/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh" # Proyectos personales # export PERSONAL_PROJECTS_DIR="${WORKSPACE_DIR:-$HOME/dev}/local" diff --git a/scripts/check-cortex-shell-integration.sh b/scripts/check-cortex-shell-integration.sh index dd85d51..38df8af 100755 --- a/scripts/check-cortex-shell-integration.sh +++ b/scripts/check-cortex-shell-integration.sh @@ -5,27 +5,27 @@ repo_root="$(cd "$(dirname "$0")/.." && pwd)" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT -dotfiles_fragment="$tmp_dir/cortex-dotfiles.zsh" -cortex_fragment="$tmp_dir/cortex.zsh" +home_dir="$tmp_dir/home" +dotfiles_fragment="$home_dir/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh" +cortex_fragment="$home_dir/.cortex/shell/cortex.zsh" +mkdir -p "$(dirname "$dotfiles_fragment")" "$(dirname "$cortex_fragment")" printf '%s\n' '_SHELL_LOAD_ORDER+=(dotfiles)' > "$dotfiles_fragment" printf '%s\n' '_SHELL_LOAD_ORDER+=(cortex)' > "$cortex_fragment" +printf '%s\n' '_UNTRUSTED_ENTRYPOINT_LOADED=1' > "$tmp_dir/untrusted.zsh" env \ - CORTEX_DOTFILES_SHELL_ENTRYPOINT="$dotfiles_fragment" \ + HOME="$home_dir" \ + CORTEX_DOTFILES_SHELL_ENTRYPOINT="$tmp_dir/untrusted.zsh" \ CORTEX_SHELL_INTEGRATION="$cortex_fragment" \ zsh -f -c ' _SHELL_LOAD_ORDER=() source "$1" [[ "${(j/:/)_SHELL_LOAD_ORDER}" == "dotfiles:cortex" ]] + (( ! ${+_UNTRUSTED_ENTRYPOINT_LOADED} )) (( ! ${+_CORTEX_DOTFILES_SHELL_PATH} )) (( ! ${+_CORTEX_SHELL_PATH} )) ' _ "$repo_root/zsh/zshrc" -home_dir="$tmp_dir/home" -mkdir -p "$home_dir/.config/cortex-dotfiles/shell" "$home_dir/.cortex/shell" -cp "$dotfiles_fragment" "$home_dir/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh" -cp "$cortex_fragment" "$home_dir/.cortex/shell/cortex.zsh" - env -u CORTEX_HOME -u CORTEX_ROOT -u CORTEX_DOTFILES_SHELL_ENTRYPOINT \ -u CORTEX_SHELL_INTEGRATION HOME="$home_dir" zsh -f -c ' _SHELL_LOAD_ORDER=() @@ -39,7 +39,7 @@ env HOME="$tmp_dir/missing-home" zsh -f -c 'source "$1"' _ "$repo_root/zsh/zshrc printf '%s\n' 'return 1' > "$dotfiles_fragment" env \ - CORTEX_DOTFILES_SHELL_ENTRYPOINT="$dotfiles_fragment" \ + HOME="$home_dir" \ CORTEX_SHELL_INTEGRATION="$cortex_fragment" \ zsh -f -c ' _SHELL_LOAD_ORDER=() @@ -51,10 +51,21 @@ grep -Fq "zsh: no se pudo cargar cortex-dotfiles: $dotfiles_fragment" "$tmp_dir/ printf '%s\n' 'if [[' > "$dotfiles_fragment" env \ - CORTEX_DOTFILES_SHELL_ENTRYPOINT="$dotfiles_fragment" \ + HOME="$home_dir" \ CORTEX_SHELL_INTEGRATION="$cortex_fragment" \ zsh -f -c ' _SHELL_LOAD_ORDER=() source "$1" 2>/dev/null [[ "${(j/:/)_SHELL_LOAD_ORDER}" == "cortex" ]] ' _ "$repo_root/zsh/zshrc" + +rm -f "$dotfiles_fragment" +ln -s "$repo_root/zsh/cortex-dotfiles.zsh" "$dotfiles_fragment" +env HOME="$home_dir" CORTEX_SHELL_INTEGRATION="$tmp_dir/missing-cortex.zsh" zsh -f -c ' + setopt ERR_EXIT + source "$1" >/dev/null + source "$1" >/dev/null + for expected in "$HOME/.local/bin" "$HOME/.opencode/bin" "$HOME/.bun/bin" "/opt/homebrew/opt/bc/bin"; do + (( ${path[(I)$expected]} == ${path[(i)$expected]} )) + done +' _ "$repo_root/zsh/zshrc" diff --git a/scripts/check-install-symlinks.sh b/scripts/check-install-symlinks.sh new file mode 100755 index 0000000..606835b --- /dev/null +++ b/scripts/check-install-symlinks.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +TIMESTAMP=test +DRY_RUN=false +# shellcheck source=scripts/install-helpers.sh +source "$repo_root/scripts/install-helpers.sh" + +new_target="$tmp_dir/new-target" +printf '%s\n' new > "$new_target" + +regular="$tmp_dir/regular" +printf '%s\n' original > "$regular" +backup_if_exists "$regular" >/dev/null +[[ "$(<"$regular.bak_test")" == original ]] +create_symlink "$new_target" "$regular" >/dev/null +[[ "$(readlink "$regular")" == "$new_target" ]] + +old_target="$tmp_dir/old-target" +printf '%s\n' old > "$old_target" +link="$tmp_dir/link" +ln -s "$old_target" "$link" +backup_if_exists "$link" >/dev/null +[[ -L "$link.bak_test" ]] +[[ "$(readlink "$link.bak_test")" == "$old_target" ]] +create_symlink "$new_target" "$link" >/dev/null + +directory="$tmp_dir/directory" +mkdir -p "$directory" +printf '%s\n' marker > "$directory/marker" +backup_if_exists "$directory" >/dev/null +[[ -f "$directory.bak_test/marker" ]] +create_symlink "$new_target" "$directory" >/dev/null +[[ -L "$directory" ]] +[[ ! -e "$directory/new-target" ]] + +blocked="$tmp_dir/blocked" +printf '%s\n' keep > "$blocked" +if create_symlink "$new_target" "$blocked" >/dev/null 2>&1; then + echo "create_symlink accepted an unbacked destination" >&2 + exit 1 +fi +[[ "$(<"$blocked")" == keep ]] + +collision="$tmp_dir/collision" +printf '%s\n' original > "$collision" +printf '%s\n' existing > "$collision.bak_test" +if backup_if_exists "$collision" >/dev/null 2>&1; then + echo "backup_if_exists accepted an existing backup path" >&2 + exit 1 +fi +[[ "$(<"$collision")" == original ]] +[[ "$(<"$collision.bak_test")" == existing ]] diff --git a/scripts/install-helpers.sh b/scripts/install-helpers.sh new file mode 100644 index 0000000..af65617 --- /dev/null +++ b/scripts/install-helpers.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +backup_if_exists() { + local src="$1" + if [[ -e "$src" || -L "$src" ]]; then + local backup="${src}.bak_${TIMESTAMP}" + if [[ "$DRY_RUN" == true ]]; then + echo " → Would backup $src to $backup" + else + if [[ -e "$backup" || -L "$backup" ]]; then + echo " ✗ Backup existente: $backup" >&2 + return 1 + fi + mv "$src" "$backup" + echo " → Backup: $backup" + fi + fi +} + +create_symlink() { + local src="$1" + local dst="$2" + if [[ "$DRY_RUN" == true ]]; then + echo " → Would symlink $dst → $src" + else + mkdir -p "$(dirname "$dst")" + if [[ -e "$dst" || -L "$dst" ]]; then + echo " ✗ Destino existente sin backup: $dst" >&2 + return 1 + fi + ln -s "$src" "$dst" + [[ -L "$dst" && "$(readlink "$dst")" == "$src" ]] || return 1 + echo " ✓ $dst → $src" + fi +} diff --git a/zsh/cortex-dotfiles.zsh b/zsh/cortex-dotfiles.zsh index 64e1245..413b9c6 100644 --- a/zsh/cortex-dotfiles.zsh +++ b/zsh/cortex-dotfiles.zsh @@ -15,8 +15,12 @@ fi #endregion #region PATH -export PATH="$HOME/.local/bin:$PATH" -[[ -d "/Applications/cmux.app/Contents/Resources/bin" ]] && export PATH="/Applications/cmux.app/Contents/Resources/bin:$PATH" +_cortex_dotfiles_path_prepend() { + [[ ":$PATH:" == *":$1:"* ]] || export PATH="$1:$PATH" +} + +_cortex_dotfiles_path_prepend "$HOME/.local/bin" +[[ -d "/Applications/cmux.app/Contents/Resources/bin" ]] && _cortex_dotfiles_path_prepend "/Applications/cmux.app/Contents/Resources/bin" #endregion #region Zsh Options @@ -348,12 +352,13 @@ _welcome #endregion # opencode -export PATH=$HOME/.opencode/bin:$PATH +_cortex_dotfiles_path_prepend "$HOME/.opencode/bin" # bun export BUN_INSTALL="$HOME/.bun" -export PATH="$BUN_INSTALL/bin:$PATH" +_cortex_dotfiles_path_prepend "$BUN_INSTALL/bin" # bun completions [ -s "$BUN_INSTALL/_bun" ] && source "$BUN_INSTALL/_bun" -export PATH="/opt/homebrew/opt/bc/bin:$PATH" +_cortex_dotfiles_path_prepend "/opt/homebrew/opt/bc/bin" +unfunction _cortex_dotfiles_path_prepend diff --git a/zsh/zshrc b/zsh/zshrc index cdcf712..781959b 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -2,16 +2,16 @@ _cortex_dotfiles_source_optional() { local label="$1" - local path="$2" + local fragment_path="$2" - [[ -r "$path" ]] || return 0 - if ! source "$path"; then - print -u2 -- "zsh: no se pudo cargar $label: $path" + [[ -r "$fragment_path" ]] || return 0 + if ! source "$fragment_path"; then + print -u2 -- "zsh: no se pudo cargar $label: $fragment_path" fi return 0 } -_CORTEX_DOTFILES_SHELL_PATH="${CORTEX_DOTFILES_SHELL_ENTRYPOINT:-$HOME/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh}" +_CORTEX_DOTFILES_SHELL_PATH="$HOME/.config/cortex-dotfiles/shell/cortex-dotfiles.zsh" _CORTEX_SHELL_PATH="${CORTEX_SHELL_INTEGRATION:-${CORTEX_HOME:-$HOME/.cortex}/shell/cortex.zsh}" _cortex_dotfiles_source_optional "cortex-dotfiles" "$_CORTEX_DOTFILES_SHELL_PATH"