diff --git a/AGENTS.md b/AGENTS.md index e8ee749..01befd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,17 +12,18 @@ Read `README.md` and `docs/automation.md` first. This directory is an independen - Vim defaults live in a managed external vimrc. Preserve the user's ~/.vimrc around one exact source line, back up both changed entry and managed files, reject symlinks, and keep personal overrides after the source line. - Install Powerlevel10k only after a healthy Oh My Zsh is present. Use the official shallow Git checkout under the effective ZSH_CUSTOM, make it the default only when the user has not selected a theme, preserve personal themes and ~/.p10k.zsh, and update only a clean official checkout under explicit --update. - Keep managed Zsh behavior in the modules under `~/.config/1bite/zsh/`; update exact managed bytes with backups and keep `.zshrc` as a one-line loader plus untouched personal content. Install the declared Nerd Font casks and verify their regular faces without changing a user's terminal font choice. +- Before loading Oh My Zsh, keep Homebrew's Zsh completion directory free of dangling links that make `compinit` write after Powerlevel10k's instant prompt. Automatically remove only the exact broken `_docker` and `_docker-compose` links created by the Docker Desktop cask; preserve and report unknown links, and never hide the error by changing a user's instant-prompt mode or `~/.p10k.zsh`. - Print the Zsh ownership boundary and restart command after configuration. A non-empty entry gets an explicit review notice; an unchanged rerun reports no change. Unknown loader syntax and dotfile-manager symlinks stop before any configuration write and give the canonical loader for manual merging; never recommend replacing a complete entry file. - Install Obsidian through its official cask. Keep the reviewed starter under `~/.config/1bite/obsidian-vault`, create only new vault directories through `obn`, use `ob` only to open Obsidian or an existing vault, and never read or update created vaults. Bundle only licensed theme assets and portable vault settings; exclude notes, workspace state, identity, account data, and community plugin data. - Recognize only the documented One Bite loader variants. Canonicalize them in place, preserve all surrounding content, and stop before writes when a managed-path loader has unknown syntax. - Default desktop policy prepares official DMGs for manual installation; iTerm2/VS Code remain casks. AI CLIs use official scripts; default Kiro keeps vendor onboarding/integration. --managed-desktop explicitly enables automatic app placement and Kiro shell management. Never label prepared downloads as installed apps. Preserve healthy unmanaged copies and personal configuration; test both policies on disposable machines. -- Docker Desktop is disabled by default. Only explicit `--with-docker` selects its installation, verification, inventory, maintenance, network probe, or manual steps; `--managed-desktop` never implies selection. Keep Docker-compatible aliases, the Oh My Zsh client plugin, and `DOCKER_DEFAULT_PLATFORM` engine-neutral so they remain useful with another local engine or a remote context. +- Docker Desktop is disabled by default. Only explicit `--with-docker` selects its installation, verification, inventory, maintenance, network probe, or manual steps; `--managed-desktop` never implies selection. Expose the Docker application PATH, aliases, helper, and Oh My Zsh client plugin only when a real `docker` client is available. Keep `DOCKER_DEFAULT_PLATFORM` engine-neutral and globally available for another local engine or a remote context. - Install the managed Python runtime first, then check Doubao and optional Sogou independently by their expected Input Methods bundle, executable, and bundle identifier. Preserve each healthy installation; otherwise prepare only that input method's checksum-verified official ZIP before remaining package/configuration/onboarding/verification stages. One installed app/package must never suppress another selected app's download. A later failure must retain current receipts and print exact package/manual steps. Installation, logout/login, input-source activation, microphone access, and typing/voice checks remain manual; receipts never claim the input method is installed or enabled. - Keep the One Bite iTerm2 Dynamic Profile on its release-stable unique GUID and the declared MesloLGS Nerd Font PostScript name. Replace only the managed profile, preserve all unrelated profiles, and tell users to open a new One Bite profile window before running the Powerlevel10k wizard. - Manage the iTerm2 global default GUID separately because Dynamic Profiles ignore the deprecated `Default Bookmark` field. Record ownership, update a still-managed legacy GUID, preserve a later user-selected default, and verify both states without modifying unrelated preferences. - Load fzf widgets only in an interactive terminal. Support both current `fzf --zsh` output and the shell files shipped by older healthy Homebrew releases; verify Ctrl+R through a pseudo-terminal and preserve later personal bindings. - Treat the Homebrew `node` formula as the complete baseline Node.js development/runtime environment. Require `node`, `npm`, and `npx`, and exercise an offline local npm build/start lifecycle during disposable-machine verification. -- Give a fresh shell the reviewed built-in Oh My Zsh plugins `git`, `macos`, `vscode`, `web-search`, `extract`, `docker`, and `tmux`. Preserve any existing `plugins` array exactly. Keep fzf, AutoJump, and zoxide on their separately verified native integrations so they are not initialized twice by Oh My Zsh plugins. +- Give a fresh shell the reviewed built-in Oh My Zsh plugins `git`, `macos`, `vscode`, `web-search`, `extract`, and `tmux`, plus `docker` only when its client is available. Preserve any existing `plugins` array exactly. Keep fzf, AutoJump, and zoxide on their separately verified native integrations so they are not initialized twice by Oh My Zsh plugins. - At the end of an install, explain every remaining DMG task with the current session's exact package path, a safely quoted `open` command, numbered copy/eject/launch steps, application-specific first-launch work, and the matching strict verification command. Save the same guide in the run directory. Derive it only from current-session receipts; do not surface stale downloads or unselected applications. If a later stage fails, still explain packages successfully prepared in that run. - Claude Desktop and Claude Code CLI are disabled by default. Only --with-claude selects their installation, configuration, verification and maintenance; --managed-desktop does not imply selection. Preserve unselected software/configuration. - Resume by checking actual state. Never trust a completion marker as proof of installation. diff --git a/README.md b/README.md index 1689d26..24376e2 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Repeat the same selection flags when rerunning or verifying. `--managed-desktop` - Powerlevel10k preserves explicit themes and `~/.p10k.zsh`. The managed One Bite iTerm2 profile selects the installed MesloLGS Nerd Font. Open a new iTerm2 window with that profile before running `p10k configure`; existing windows keep their previous profile and font. - After iTerm2 is reopened, One Bite is the default profile. If you later choose another default, reruns preserve your selection. Other profiles and their settings are never rewritten. - In an interactive terminal, fzf provides Ctrl+R history search, Ctrl+T file search, and Option+C directory search across current and older supported Homebrew releases. -- A fresh shell enables the built-in Oh My Zsh plugins for Git, macOS, VS Code, web search, archive extraction, Docker-compatible CLIs, and tmux. Existing plugin selections remain untouched. The plugin and shell aliases do not install Docker Desktop and also work with compatible engines. fzf, AutoJump, and zoxide use their native integrations to avoid duplicate hooks. +- A fresh shell enables the built-in Oh My Zsh plugins for Git, macOS, VS Code, web search, archive extraction, and tmux. When a Docker-compatible client is actually available, it also enables the Docker plugin, aliases, helper, and application CLI path. Existing plugin selections remain untouched. fzf, AutoJump, and zoxide use their native integrations to avoid duplicate hooks. - `ob` opens Obsidian or an existing vault. `obn` copies the managed starter only into a new directory. Existing vaults remain user-owned and later One Bite updates affect only future vaults. ## Terminal welcome and fetch tools diff --git a/VERSION b/VERSION index bcab45a..81340c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.3 +0.0.4 diff --git a/config/env.zsh b/config/env.zsh index 321c951..e6d3027 100644 --- a/config/env.zsh +++ b/config/env.zsh @@ -7,7 +7,10 @@ for setup_brew in /opt/homebrew/bin/brew /usr/local/bin/brew; do fi done unset setup_brew -path=("$HOME/.local/bin" /Applications/Docker.app/Contents/Resources/bin $path) +path=("$HOME/.local/bin" $path) +setup_docker_bin=/Applications/Docker.app/Contents/Resources/bin +[[ -x "$setup_docker_bin/docker" ]] && path+=("$setup_docker_bin") +unset setup_docker_bin # Produce portable x86-64 Linux images by default on Apple Silicon. A project # or one-off command can set a different platform before loading this file. diff --git a/config/zsh/development.zsh b/config/zsh/development.zsh index 8942a96..9a5d2fc 100644 --- a/config/zsh/development.zsh +++ b/config/zsh/development.zsh @@ -1,16 +1,18 @@ # Managed by 1bite: development. Personal overrides belong after the source in .zshrc. alias ll='ls -lah' alias g='git' -alias cl='claude' -alias clc='claude --continue' -# Explicit opt-in shortcuts: skip Claude permission checks / Codex approvals and sandbox. -alias cld='claude --dangerously-skip-permissions' -alias cldc='claude --dangerously-skip-permissions --continue' +if (( $+commands[claude] || $+functions[claude] )); then + alias cl='claude' + alias clc='claude --continue' + # Explicit opt-in shortcuts: skip Claude permission checks. + alias cld='claude --dangerously-skip-permissions' + alias cldc='claude --dangerously-skip-permissions --continue' +fi alias cx='codex' alias cxc='codex resume --last' +# Explicit opt-in shortcuts: skip Codex approvals and sandbox. alias cxd='codex --dangerously-bypass-approvals-and-sandbox' alias cxdc='codex resume --last --dangerously-bypass-approvals-and-sandbox' -alias dc='docker compose' # Portable local shortcuts; these override colliding Oh My Zsh aliases. @@ -75,24 +77,27 @@ alias gmg='go mod graph' alias gmc='go clean --modcache' # Docker; d stays the directory stack function defined below. -alias dk=docker -alias dkc='docker container' -alias dkcm='docker compose' -alias dexec='docker exec -it' -alias di='docker images' -alias dimg='docker images' -alias dkimg='docker image ls' -alias dklg='docker logs -f' -alias dkls='docker ps -a' -alias dkps='docker ps -a' -alias dkrm='docker rm -f' -alias dps='docker ps' -alias dpsa='docker ps -a' -alias drmi='docker rmi' -alias dks='docker service' -alias dksm='docker swarm' -alias dkst='docker stack' -alias dkstat='docker system df' +if (( $+commands[docker] || $+functions[docker] )); then + alias dc='docker compose' + alias dk=docker + alias dkc='docker container' + alias dkcm='docker compose' + alias dexec='docker exec -it' + alias di='docker images' + alias dimg='docker images' + alias dkimg='docker image ls' + alias dklg='docker logs -f' + alias dkls='docker ps -a' + alias dkps='docker ps -a' + alias dkrm='docker rm -f' + alias dps='docker ps' + alias dpsa='docker ps -a' + alias drmi='docker rmi' + alias dks='docker service' + alias dksm='docker swarm' + alias dkst='docker stack' + alias dkstat='docker system df' +fi # Disk alias df='df -h' @@ -100,6 +105,6 @@ alias df='df -h' # Multi-command shortcuts stop immediately if an earlier command fails. unalias gpre gps1 mkcd mcd d cdf o dl mktgz mkzip tfind ff jv jp jsonview pcat \ sha1 sha224 sha256 sha384 sha512 sha512224 sha512256 gci gcia git_corb \ - git_ignore git_readme tn tad to tkss tmuxconf tds cn gcv dkclear fingerprint \ + git_ignore git_readme tn tad to tkss tmuxconf tds cn gcv fingerprint \ ffmpeg2wav ffmpeg2pcm video2wav pcm2wav heic2jpg png2jpg webp2png svg2png \ transpng img_trans img_pure_jpg img_pure_png new_bash 2>/dev/null || true diff --git a/config/zsh/framework.zsh b/config/zsh/framework.zsh index e58e186..846d084 100644 --- a/config/zsh/framework.zsh +++ b/config/zsh/framework.zsh @@ -9,7 +9,11 @@ if (( ! $+functions[omz] )) && [[ -r "${ZSH:-$HOME/.oh-my-zsh}/oh-my-zsh.sh" ]]; ZSH_THEME=robbyrussell fi fi - (( ${+plugins} )) || plugins=(git macos vscode web-search extract docker tmux) + if (( ! ${+plugins} )); then + plugins=(git macos vscode web-search extract) + (( $+commands[docker] )) && plugins+=(docker) + plugins+=(tmux) + fi zstyle ':omz:update' mode disabled source "$ZSH/oh-my-zsh.sh" fi diff --git a/config/zsh/terminal.zsh b/config/zsh/terminal.zsh index 3af17dd..2b4d3a8 100644 --- a/config/zsh/terminal.zsh +++ b/config/zsh/terminal.zsh @@ -61,9 +61,12 @@ function tds { } # Docker cleanup -function dkclear { - docker system prune -f -} +if (( $+commands[docker] || $+functions[docker] )); then + unalias dkclear 2>/dev/null || true + function dkclear { + docker system prune -f + } +fi # Misc alias reload='. ~/.zshrc' diff --git a/docs/automation.md b/docs/automation.md index 42faeff..d184340 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -39,11 +39,11 @@ Managed AI app placement checks vendor identity, Apple-trusted signature, execut Records under ${XDG_STATE_HOME:-~/.local/state}/1bite/official-installs identify managed ownership, never prove health. Preserve personal configuration and credentials. cask:* IDs remain stable in machine reports even where the component uses an official download rather than Homebrew; terminal progress and errors display app:* to avoid implying Homebrew ownership. Claude Desktop uses cask:claude-desktop, distinct from the claude CLI stage. Recognize ChatGPT.app and Codex.app with bundle ID com.openai.codex; desktop and CLI health are independent. -Apply configuration before installing Oh My Zsh so `KEEP_ZSHRC=yes` preserves the managed source line. Install Powerlevel10k only after Oh My Zsh is healthy, using the official shallow checkout under `${ZSH_CUSTOM:-$ZSH/custom}/themes/powerlevel10k`. When the user has not set `ZSH_THEME`, select Powerlevel10k if its theme file exists and fall back to robbyrussell for configuration-only or incomplete pre-install states; preserve explicit themes, including an empty value, plugins and `~/.p10k.zsh`. Install the declared MesloLGS and JetBrains Mono Nerd Font casks and verify their regular faces under `~/Library/Fonts`. The default One Bite iTerm2 profile uses MesloLGS Nerd Font 13; other profiles and already-open windows retain their selection. The final guide reopens iTerm2 before `p10k configure`, while non-iTerm shells may use `exec zsh`. Healthy repositories and fonts are skipped on rerun; --update only fast-forwards clean official checkouts and asks Homebrew to upgrade managed font casks. Custom origins, symlinks, incomplete directories, local changes and external font files are never overwritten. Tests cover custom ZSH/ZSH_CUSTOM directories, unchanged repeats, atomic first install, one framework load and theme preservation. System Zsh remains the login shell. Fresh VS Code settings select Zsh login shell and Default Dark Modern; preserve existing JSONC bytes. Install/check every ID in `config/vscode-extensions.txt` independently; failures stop the run. Default execution skips installed extensions; --update explicitly requests upgrades. +Apply configuration before installing Oh My Zsh so `KEEP_ZSHRC=yes` preserves the managed source line. Before that future shell load, inspect Homebrew's Zsh completion directory: remove only dangling `_docker` and `_docker-compose` links whose resolved targets exactly match the two Docker Desktop cask artifacts, preserve and report every unknown link, and make verification reject remaining damage. This repairs `compinit` console output instead of hiding it through Powerlevel10k's quiet mode, and never edits `~/.p10k.zsh`. Install Powerlevel10k only after Oh My Zsh is healthy, using the official shallow checkout under `${ZSH_CUSTOM:-$ZSH/custom}/themes/powerlevel10k`. When the user has not set `ZSH_THEME`, select Powerlevel10k if its theme file exists and fall back to robbyrussell for configuration-only or incomplete pre-install states; preserve explicit themes, including an empty value, plugins and `~/.p10k.zsh`. Install the declared MesloLGS and JetBrains Mono Nerd Font casks and verify their regular faces under `~/Library/Fonts`. The default One Bite iTerm2 profile uses MesloLGS Nerd Font 13; other profiles and already-open windows retain their selection. The final guide reopens iTerm2 before `p10k configure`, while non-iTerm shells may use `exec zsh`. Healthy repositories and fonts are skipped on rerun; --update only fast-forwards clean official checkouts and asks Homebrew to upgrade managed font casks. Custom origins, symlinks, incomplete directories, local changes and external font files are never overwritten. Tests cover custom ZSH/ZSH_CUSTOM directories, unchanged repeats, atomic first install, one framework load and theme preservation. System Zsh remains the login shell. Fresh VS Code settings select Zsh login shell and Default Dark Modern; preserve existing JSONC bytes. Install/check every ID in `config/vscode-extensions.txt` independently; failures stop the run. Default execution skips installed extensions; --update explicitly requests upgrades. -A fresh shell uses the reviewed built-in Oh My Zsh plugin set `git macos vscode web-search extract docker tmux`. Preserve an existing `plugins` array exactly. fzf, AutoJump, and zoxide remain outside that array because One Bite initializes and tests their native integrations directly; do not create duplicate OMZ hooks for them. +A fresh shell uses the reviewed built-in Oh My Zsh plugin set `git macos vscode web-search extract tmux`, inserting `docker` before `tmux` only when an actual Docker client is available. Preserve an existing `plugins` array exactly. fzf, AutoJump, and zoxide remain outside that array because One Bite initializes and tests their native integrations directly; do not create duplicate OMZ hooks for them. -`env.zsh` supplies deduplicated PATH in login and interactive shells; `shell.zsh` is the interactive loader for eight files under `zsh/`: framework, options/history, tool hooks, development aliases, utilities, media helpers, Git functions and terminal/tmux helpers. Write module dependencies first and the loader last. Exact managed files are backed up and atomically replaced on version changes; verification rejects missing, symlinked or byte-different modules. Preserve `.zshrc` content around its one loader line and let later personal definitions override managed defaults. Preserve an explicit GOPATH/GOBIN, otherwise resolve persisted `go env` values, export the effective workspace/bin, append its tool directory to PATH as documented by Go for macOS and never set GOROOT. At the configuration CLI boundary, absolute CODEX_HOME, CLAUDE_CONFIG_DIR (only with --with-claude) and ZDOTDIR override the respective default directories; pass the same environment on rerun/verify. No credentials or proxy values are written into public templates or exported to GUI launch services. The Claude template is only read/applied/verified when selected. External configuration copies must include every selected top-level template, all `zsh/` modules, and the full `obsidian-vault/` tree. Package, extension, source, and font manifests remain repository-controlled. +`env.zsh` supplies deduplicated PATH in login and interactive shells; it adds Docker.app's CLI directory only when that directory contains an executable client. `shell.zsh` is the interactive loader for eight files under `zsh/`: framework, options/history, tool hooks, development aliases, utilities, media helpers, Git functions and terminal/tmux helpers. Write module dependencies first and the loader last. Exact managed files are backed up and atomically replaced on version changes; verification rejects missing, symlinked or byte-different modules. Preserve `.zshrc` content around its one loader line and let later personal definitions override managed defaults. Preserve an explicit GOPATH/GOBIN, otherwise resolve persisted `go env` values, export the effective workspace/bin, append its tool directory to PATH as documented by Go for macOS and never set GOROOT. At the configuration CLI boundary, absolute CODEX_HOME, CLAUDE_CONFIG_DIR (only with --with-claude) and ZDOTDIR override the respective default directories; pass the same environment on rerun/verify. No credentials or proxy values are written into public templates or exported to GUI launch services. The Claude template is only read/applied/verified when selected. External configuration copies must include every selected top-level template, all `zsh/` modules, and the full `obsidian-vault/` tree. Package, extension, source, and font manifests remain repository-controlled. Preflight both Zsh entry files before any configuration write. Recognized One Bite loader variants may be canonicalized; a likely source command that references a managed path but has unknown syntax stops with its line number and the canonical line for manual merging. After a write, distinguish an initialized empty entry, a reviewed change to a non-empty entry, and a byte-identical rerun in terminal output. Never prompt interactively inside automation. @@ -69,13 +69,13 @@ The eight default extensions are Go, Python, Ruff, ESLint, Prettier, YAML, Shell ## Development shell shortcuts -The interactive template explicitly defines portable Make, Git, Go and Docker aliases from the local conventions; shell-editor.md contains the complete mappings. They load after Oh My Zsh, so gd intentionally means git diff --no-index (use gdiff for repository diff) and gm means go mod (use git merge for merging). d is the Oh My Zsh-compatible directory stack function (dirs -v | head -n 10), also defined without OMZ; Docker uses dk/dc. The template also ports portable file, viewer, clipboard, hash, media and tmux helpers (mcd/mkcd, cdf, o, dl, mktgz/mkzip, tfind/ff, trim, lsp/lsmax/lslast, jv/jp/jsonview, ccat/mcat/readme, pc/pp/ppwd/pd/pcat/l2l, now/utcnow, SHA families, media/image conversion, new_bash, t/ts/ta/tk/tn and ta0–ta16, reload/cls/e/ns/weather/webserver/fingerprint). Media helpers use the already declared ffmpeg/ImageMagick or system sips and refuse missing inputs; generated scripts refuse overwrite. Alias names are lowercase. Additional CLI arguments retain normal shell quoting. These definitions invoke no mutations at shell startup; tests expand linear aliases against stubs and pin composite pipeline aliases by their raw definition text. +The interactive template explicitly defines portable Make, Git and Go aliases from the local conventions, plus Docker aliases only when a Docker client is available; shell-editor.md contains the complete mappings. They load after Oh My Zsh, so gd intentionally means git diff --no-index (use gdiff for repository diff) and gm means go mod (use git merge for merging). d is the Oh My Zsh-compatible directory stack function (dirs -v | head -n 10), also defined without OMZ; Docker uses dk/dc when enabled. The template also ports portable file, viewer, clipboard, hash, media and tmux helpers (mcd/mkcd, cdf, o, dl, mktgz/mkzip, tfind/ff, trim, lsp/lsmax/lslast, jv/jp/jsonview, ccat/mcat/readme, pc/pp/ppwd/pd/pcat/l2l, now/utcnow, SHA families, media/image conversion, new_bash, t/ts/ta/tk/tn and ta0–ta16, reload/cls/e/ns/weather/webserver/fingerprint). Media helpers use the already declared ffmpeg/ImageMagick or system sips and refuse missing inputs; generated scripts refuse overwrite. Alias names are lowercase. Additional CLI arguments retain normal shell quoting. These definitions invoke no mutations at shell startup; tests expand linear aliases against stubs and pin composite pipeline aliases by their raw definition text. -gpre and gps1 are functions: gpre runs status, add --all and diff --staged -w, stopping on failure; gps1 resolves the current symbolic branch and pushes with upstream, stopping before push on detached HEAD. gci/gcia are functions that require a commit message and fail without committing when it is missing. dkclear explicitly invokes docker system prune -f, removing stopped containers, unused networks, dangling images and build cache, without volumes; it propagates errors. Arguments append to the final diff/push command. Make targets belong to the current project. The gmd shortcut uses a master-only checkout/pull/prune workflow. No private revive configuration, obsolete Go compatibility/insecure flags, proxy changes, private helper tools or hardcoded personal paths are included. Existing personal files remain untouched; place overrides after the managed source line. +gpre and gps1 are functions: gpre runs status, add --all and diff --staged -w, stopping on failure; gps1 resolves the current symbolic branch and pushes with upstream, stopping before push on detached HEAD. gci/gcia are functions that require a commit message and fail without committing when it is missing. When Docker is available, dkclear explicitly invokes docker system prune -f, removing stopped containers, unused networks, dangling images and build cache, without volumes; it propagates errors. Arguments append to the final diff/push command. Make targets belong to the current project. The gmd shortcut uses a master-only checkout/pull/prune workflow. No private revive configuration, obsolete Go compatibility/insecure flags, proxy changes, private helper tools or hardcoded personal paths are included. Existing personal files remain untouched; place overrides after the managed source line. ## AI shell aliases -Interactive shell.zsh defines cl/clc/cld/cldc and cx/cxc/cxd/cxdc. Plain aliases launch the CLI or resume (--continue for Claude; resume --last for Codex). The d variants explicitly add --dangerously-skip-permissions for Claude or --dangerously-bypass-approvals-and-sandbox for Codex, including the corresponding resume variant. These aliases are opt-in command invocations, not persistent CLI permission policy; only Codex's bypass flag explicitly disables its sandbox. Preserve personal CLI configuration and allow alias overrides after the managed source line. Never run bypass sessions during setup or automated tests; validate expansion using stub executables in isolated Zsh. Noninteractive shells do not define these aliases. +Interactive shell.zsh defines cx/cxc/cxd/cxdc for the default Codex installation, and defines cl/clc/cld/cldc only when a Claude command or function is available. Plain aliases launch the CLI or resume (--continue for Claude; resume --last for Codex). The d variants explicitly add --dangerously-skip-permissions for Claude or --dangerously-bypass-approvals-and-sandbox for Codex, including the corresponding resume variant. These aliases are opt-in command invocations, not persistent CLI permission policy; only Codex's bypass flag explicitly disables its sandbox. Preserve personal CLI configuration and allow alias overrides after the managed source line. Never run bypass sessions during setup or automated tests; validate expansion using stub executables in isolated Zsh. Noninteractive shells do not define these aliases. Existing installations can apply this template with --configure-only and open a new terminal. A caller using --config-dir must update their external shell.zsh and keep the same option. Local shell files are reference material only: no local credentials, paths, proxy wrappers or internal aliases are published. diff --git a/docs/installation.md b/docs/installation.md index 19d447d..69d1bd5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -30,7 +30,7 @@ Default mode installs command-line tools, iTerm2, VS Code, the selected AI CLIs, `prepared` means the package is complete; it does not mean the application is installed. `result.json` records `desktop_mode`, `with_docker`, `with_claude`, and `manual_steps`. `desktop-installers.json` records packages prepared during that run. A successful default run means automated work and package preparation succeeded. Missing applications remain `null` in inventory and appear under `pending_applications`. -Docker Desktop is enabled with `--with-docker`. Without that flag it is not downloaded, checked, updated, or added to inventory and manual steps, even if Docker Desktop is already present. Repeat the flag for reruns, verification, and maintenance. `--managed-desktop` never enables it implicitly. The Docker-compatible shell aliases and `DOCKER_DEFAULT_PLATFORM` setting remain available for another local engine or a remote Docker context. +Docker Desktop is enabled with `--with-docker`. Without that flag it is not downloaded, checked, updated, or added to inventory and manual steps, even if Docker Desktop is already present. Repeat the flag for reruns, verification, and maintenance. `--managed-desktop` never enables it implicitly. The shell exposes Docker aliases, its helper, plugin, and Docker.app CLI path only when a compatible client is actually available. `DOCKER_DEFAULT_PLATFORM` remains available for another local engine or a remote Docker context. Claude Desktop and Claude Code are enabled together with `--with-claude`. Without that flag they are not downloaded, configured, checked, updated, or removed. Repeat the same flag with rerun, verify, configure, update, and update-check commands. `--managed-desktop` never enables Claude implicitly. diff --git a/docs/shell-editor.md b/docs/shell-editor.md index c1681f0..f2f6b79 100644 --- a/docs/shell-editor.md +++ b/docs/shell-editor.md @@ -67,7 +67,7 @@ set undofile - Missing Oh My Zsh is installed with the official unattended installer while preserving `.zshrc`, without `chsh` or automatically starting Zsh. iTerm2 and new VS Code terminals use `/bin/zsh -l`. - A healthy Oh My Zsh installation receives a shallow Powerlevel10k checkout under `${ZSH_CUSTOM:-$ZSH/custom}/themes/powerlevel10k`. It is selected only if `ZSH_THEME` was not explicitly set. Configuration-only mode safely uses `robbyrussell` when Powerlevel10k is absent. - Existing Oh My Zsh directories, explicit themes including an empty theme, plugin lists, and `~/.p10k.zsh` are preserved. Normal reruns skip healthy checkouts. `--update` fast-forwards only a clean official checkout; custom origins, forks, symlinks, and incomplete directories remain with their existing owner. -- PATH is deduplicated and includes `~/.local/bin`, Docker CLI, VS Code CLI, and Go tools. Existing `GOPATH` and `GOBIN` win; otherwise One Bite reads persisted `go env` values and falls back to `~/go`. It exports the effective workspace and adds explicit `GOBIN`, or every `GOPATH/bin`, without setting `GOROOT`. +- PATH is deduplicated and includes `~/.local/bin`, available Docker.app and VS Code CLI directories, and Go tools. An application directory is added only when its client executable exists. Existing `GOPATH` and `GOBIN` win; otherwise One Bite reads persisted `go env` values and falls back to `~/go`. It exports the effective workspace and adds explicit `GOBIN`, or every `GOPATH/bin`, without setting `GOROOT`. - `DOCKER_DEFAULT_PLATFORM` defaults to `linux/amd64`, including on Apple Silicon, so ordinary Docker-compatible builds and runs target a portable x86-64 Linux platform. An already set value is preserved. This client setting does not install Docker Desktop and works with another compatible local engine or remote context. Override it before loading the environment or for one command when a project needs `linux/arm64` or a multi-platform builder. - Existing absolute `CODEX_HOME` and `CLAUDE_CONFIG_DIR` are honored. Finder-launched applications cannot be assumed to inherit terminal environment variables. @@ -75,7 +75,7 @@ After Powerlevel10k is enabled, reopen iTerm2 and start a new window with **Prof The tools module enables fzf's Ctrl+R history search, Ctrl+T file picker, and Option+C directory picker only in an interactive terminal. Current fzf releases use `fzf --zsh`; older healthy Homebrew releases fall back to their installed `shell/completion.zsh` and `shell/key-bindings.zsh`. Personal bindings after the One Bite loader still take precedence. -When no plugin list exists, the framework enables the built-in Oh My Zsh plugins `git`, `macos`, `vscode`, `web-search`, `extract`, `docker`, and `tmux`. The Docker plugin provides client completion and does not install Docker Desktop. An existing list remains user-owned. fzf, AutoJump, and zoxide deliberately stay out of the OMZ list because the tools module loads their native integrations once and verifies their actual widgets/functions. +On a fresh machine, `.zshrc` contains only the managed loader and the default plugin array is declared inside `~/.config/1bite/zsh/framework.zsh`. The framework enables the built-in Oh My Zsh plugins `git`, `macos`, `vscode`, `web-search`, `extract`, and `tmux`, inserting `docker` only when the shell can resolve a Docker client. If `.zshrc` or another earlier file already defines `plugins=(...)`, that array remains user-owned and takes precedence byte-for-byte. fzf, AutoJump, and zoxide deliberately stay out of the managed OMZ list because the tools module loads their native integrations once and verifies their actual widgets/functions. For a maintained cross-shell prompt, consider [Starship](https://starship.rs/). [Oh My Posh](https://ohmyposh.dev/) provides a broader segment/template model, and [Spaceship](https://spaceship-prompt.sh/) stays native to Zsh. Initialize only one prompt engine. One Bite currently manages Powerlevel10k and leaves other prompt configuration untouched. @@ -85,7 +85,7 @@ The templates use lowercase local conventions: `m` for Make, `g` for Git, and `d | Group | Shortcut -> command or behavior | | --- | --- | -| Core | `g` -> `git`; `lg` -> `lazygit`; `dk` -> `docker`; `dc` -> `docker compose`; `ll` -> `ls -lah`; `d` -> directory stack | +| Core | `g` -> `git`; `lg` -> `lazygit`; `ll` -> `ls -lah`; `d` -> directory stack; `dk` -> `docker` and `dc` -> `docker compose` when Docker is available | | Make | `m`, `mb`, `mi`, `mr`, `mt`, `mp` -> `make`, `make build/install/run/test/preview` | | Git stage/commit | `ga` -> `git add`; `gaa` -> `git add --all`; `gci` / `gcia` commit or amend with a required message; `gcmsg` -> `git commit --message` | | Git status/diff | `gs`, `gst`, `gss`; `gd` -> `git diff --no-index`; `gdiff`; `gds`; `gdca` | @@ -107,7 +107,7 @@ The templates use lowercase local conventions: `m` for Make, `g` for Git, and `d | tmux | `t`, `ts`, `ta`, `tk`, `tn`, and `ta0` through `ta16` | | Miscellaneous | `reload`, `cls`, `e`, `ns`, `weather`, `webserver`, `fingerprint` | -`gpre` shows status, stages all changes, and shows the staged diff. `gps1` pushes the current branch to origin with upstream tracking and rejects detached HEAD. Each function stops at its first failure. `dkclear` explicitly runs `docker system prune -f`, removing stopped containers, unused networks, dangling images, and build cache but not volumes. +`gpre` shows status, stages all changes, and shows the staged diff. `gps1` pushes the current branch to origin with upstream tracking and rejects detached HEAD. Each function stops at its first failure. When Docker is available, `dkclear` runs `docker system prune -f`, removing stopped containers, unused networks, dangling images, and build cache but not volumes. ```bash mb # run this project's make build target @@ -144,6 +144,8 @@ The default global ignore contains operating-system metadata, `*.log`, and Go co ## Claude and Codex aliases +Codex aliases are part of the default installation. Claude aliases appear only when the optional Claude command is actually available. + | Alias | Expansion | | --- | --- | | `cl` | `claude` | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2c488db..9d9b349 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -19,6 +19,7 @@ | iTerm2 reports duplicate Dynamic Profile GUIDs | Update One Bite and run `./1bite --configure-only`. The managed profile receives its current unique GUID while unrelated profile files are preserved. Reopen iTerm2 after the update. | | New iTerm2 windows still use another profile | Run `./1bite --configure-only`, quit and reopen iTerm2, and confirm **Profiles > One Bite** exists. One Bite makes it the default once; if you later selected another default, reruns preserve your choice. | | Powerlevel10k offers to download Meslo again | Open a new window with **Profiles > One Bite**. Existing windows retain their old font. Confirm the profile's Text font is MesloLGS Nerd Font, reopen iTerm2, then run `p10k configure` again without accepting a duplicate font download. | +| Powerlevel10k warns about console output and shows `compinit: no such file or directory: .../_docker-compose` | This is a dangling Docker Desktop completion link, not a prompt/status-line error. Update One Bite and run `./1bite --configure-only`; it removes only the two known broken links created by Docker Desktop's Homebrew cask. It preserves `~/.p10k.zsh` and unknown completion links. Do not set instant prompt to `quiet` merely to hide the underlying error. | | Ctrl+R does not open fzf history search | Open a new interactive terminal, run `type fzf-history-widget`, then `bindkey '^R'`. Rerun `./1bite --configure-only` if the widget is missing. One Bite supports both current `fzf --zsh` and older Homebrew shell integration files; personal bindings after the loader may intentionally override Ctrl+R. | ## Git configuration access diff --git a/scripts/configure.py b/scripts/configure.py index c9322f5..9b60028 100644 --- a/scripts/configure.py +++ b/scripts/configure.py @@ -39,6 +39,10 @@ OBSIDIAN_COMMAND_PATHS = (Path('.local/bin/ob'), Path('.local/bin/obn')) ZSH_MODULES = ('framework.zsh', 'options.zsh', 'tools.zsh', 'development.zsh', 'utilities.zsh', 'media.zsh', 'git-functions.zsh', 'terminal.zsh') +DOCKER_ZSH_COMPLETIONS = { + '_docker': Path('/Applications/Docker.app/Contents/Resources/etc/docker.zsh-completion'), + '_docker-compose': Path('/Applications/Docker.app/Contents/Resources/etc/docker-compose.zsh-completion'), +} GIT_DEFAULT_SECTIONS = { 'alias', 'branch', 'color', 'commit', 'core', 'delta', 'diff', 'difftool', 'fetch', 'help', 'init', 'interactive', 'merge', 'mergetool', 'pull', 'push', 'rebase', 'rerere', 'tag', @@ -120,6 +124,53 @@ def unrecognized_source_line(content, recognized): return None +def homebrew_prefix(): + prefix = Path(os.environ.get('HOMEBREW_PREFIX', '/opt/homebrew')) + if not prefix.is_absolute(): + raise ValueError('HOMEBREW_PREFIX must be an absolute path') + return prefix + + +def broken_zsh_completion_links(prefix=None): + """Return dangling links that make compinit print during shell startup.""" + directory = (prefix or homebrew_prefix()) / 'share/zsh/site-functions' + if not directory.exists(): + return [] + if directory.is_symlink() or not directory.is_dir(): + raise ValueError(f'Homebrew Zsh completion path is not a regular directory: {directory}') + try: + return [path for path in sorted(directory.iterdir()) if path.is_symlink() and not path.exists()] + except OSError as error: + raise ValueError(f'Cannot inspect Homebrew Zsh completions: {directory}') from error + + +def repair_broken_zsh_completions(prefix=None): + """Remove only dangling links created by Docker Desktop's Homebrew cask.""" + prefix = prefix or homebrew_prefix() + for path in broken_zsh_completion_links(prefix): + expected = DOCKER_ZSH_COMPLETIONS.get(path.name) + if expected is None or Path(os.path.realpath(path)) != expected: + raise ValueError( + f'Broken Zsh completion symlink is not owned by One Bite: {path}. ' + 'Repair its Homebrew package or remove that one link with its original manager, then rerun.') + try: + path.unlink() + except OSError as error: + raise ValueError( + f'Cannot remove broken Docker Desktop completion link: {path}. ' + 'Repair it with the account that owns the Homebrew prefix, then rerun.') from error + print(f'Repaired: removed broken Docker Desktop completion link: {path}') + + +def verify_zsh_completions(prefix=None): + broken = broken_zsh_completion_links(prefix) + if broken: + raise ValueError( + f'Broken Zsh completion symlink causes compinit startup output: {broken[0]}. ' + 'Run ./1bite --configure-only to repair known Docker Desktop links; ' + 'repair any other link with its original package manager.') + + def relocate_profile_artifacts(home): """Keep backups and interrupted writes outside iTerm2's watched directory.""" profile = home / PROFILE_PATH @@ -513,6 +564,7 @@ def configure(home, config_dir, codex_home=None, claude_home=None, shell_home=No raise ValueError(f'Refusing to configure Kiro through symlink: {path}') if (home / KIRO_PATH).exists(): kiro_policy_status(home) + repair_broken_zsh_completions() relocate_profile_artifacts(home) if with_claude: write(claude_path, claude.encode(), preserve=True) @@ -595,6 +647,7 @@ def report_kiro(home): def verify(home, codex_home=None, claude_home=None, shell_home=None, with_claude=False, config_dir=ROOT / 'config'): + verify_zsh_completions() validate(((claude_home or home / '.claude') / 'settings.json').read_text() if with_claude else '{}', ((codex_home or home / '.codex') / 'config.toml').read_text(), (home / PROFILE_PATH).read_text()) diff --git a/tests/fixtures/config-golden.json b/tests/fixtures/config-golden.json index 35944b5..d89a79d 100644 --- a/tests/fixtures/config-golden.json +++ b/tests/fixtures/config-golden.json @@ -3,7 +3,7 @@ "claude-settings.json": "{\n \"permissions\": {\n \"defaultMode\": \"default\"\n },\n \"language\": \"Chinese\"\n}\n", "codex-config.toml": "# Use the current official model and authentication defaults.\napproval_policy = \"on-request\"\nsandbox_mode = \"workspace-write\"\nweb_search = \"cached\"\n", "download.curlrc": "# Public transport policy; credentials and proxy settings do not belong here.\nhttp1.1\nconnect-timeout = 20\nmax-time = 1800\nspeed-limit = 1\nspeed-time = 60\nretry-all-errors\nretry-max-time = 1800\n", - "env.zsh": "# Public environment only. Never put credentials or proxy values in this file.\ntypeset -U path\nfor setup_brew in /opt/homebrew/bin/brew /usr/local/bin/brew; do\n if [[ -x \"$setup_brew\" ]]; then\n eval \"$(\"$setup_brew\" shellenv)\"\n break\n fi\ndone\nunset setup_brew\npath=(\"$HOME/.local/bin\" /Applications/Docker.app/Contents/Resources/bin $path)\n\n# Produce portable x86-64 Linux images by default on Apple Silicon. A project\n# or one-off command can set a different platform before loading this file.\nexport DOCKER_DEFAULT_PLATFORM=\"${DOCKER_DEFAULT_PLATFORM:-linux/amd64}\"\n\n# Export the effective Go workspace as well as its binary directory. Respect an\n# existing shell override and Go's persisted `go env -w GOPATH/GOBIN` values.\nif [[ -z ${GOPATH:-} ]]; then\n setup_go_path=''\n (( $+commands[go] )) && setup_go_path=$(command go env GOPATH 2>/dev/null)\n export GOPATH=\"${setup_go_path:-$HOME/go}\"\n unset setup_go_path\nelse\n export GOPATH\nfi\nif [[ -n ${GOBIN:-} ]]; then\n export GOBIN\n path+=(\"$GOBIN\")\nelse\n setup_go_bin=''\n # Go 1.27 may report the first GOPATH/bin as a derived GOBIN. Read GOENV so\n # only an explicit `go env -w GOBIN=...` overrides the documented GOPATH bins.\n if (( $+commands[go] )); then\n setup_go_env=$(command go env GOENV 2>/dev/null)\n if [[ -r $setup_go_env ]]; then\n while IFS= read -r setup_go_setting; do\n case $setup_go_setting in\n GOBIN=*) setup_go_bin=${setup_go_setting#GOBIN=} ;;\n esac\n done <\"$setup_go_env\"\n fi\n unset setup_go_env setup_go_setting\n fi\n if [[ -n $setup_go_bin ]]; then\n export GOBIN=\"$setup_go_bin\"\n path+=(\"$setup_go_bin\")\n else\n for setup_go in ${(s/:/)GOPATH}; do path+=(\"$setup_go/bin\"); done\n unset setup_go\n fi\n unset setup_go_bin\nfi\nfor setup_code in /Applications/'Visual Studio Code.app'/Contents/Resources/app/bin \"$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin\"; do\n [[ -x \"$setup_code/code\" ]] && path+=(\"$setup_code\")\ndone\nunset setup_code\nexport PATH\n", + "env.zsh": "# Public environment only. Never put credentials or proxy values in this file.\ntypeset -U path\nfor setup_brew in /opt/homebrew/bin/brew /usr/local/bin/brew; do\n if [[ -x \"$setup_brew\" ]]; then\n eval \"$(\"$setup_brew\" shellenv)\"\n break\n fi\ndone\nunset setup_brew\npath=(\"$HOME/.local/bin\" $path)\nsetup_docker_bin=/Applications/Docker.app/Contents/Resources/bin\n[[ -x \"$setup_docker_bin/docker\" ]] && path+=(\"$setup_docker_bin\")\nunset setup_docker_bin\n\n# Produce portable x86-64 Linux images by default on Apple Silicon. A project\n# or one-off command can set a different platform before loading this file.\nexport DOCKER_DEFAULT_PLATFORM=\"${DOCKER_DEFAULT_PLATFORM:-linux/amd64}\"\n\n# Export the effective Go workspace as well as its binary directory. Respect an\n# existing shell override and Go's persisted `go env -w GOPATH/GOBIN` values.\nif [[ -z ${GOPATH:-} ]]; then\n setup_go_path=''\n (( $+commands[go] )) && setup_go_path=$(command go env GOPATH 2>/dev/null)\n export GOPATH=\"${setup_go_path:-$HOME/go}\"\n unset setup_go_path\nelse\n export GOPATH\nfi\nif [[ -n ${GOBIN:-} ]]; then\n export GOBIN\n path+=(\"$GOBIN\")\nelse\n setup_go_bin=''\n # Go 1.27 may report the first GOPATH/bin as a derived GOBIN. Read GOENV so\n # only an explicit `go env -w GOBIN=...` overrides the documented GOPATH bins.\n if (( $+commands[go] )); then\n setup_go_env=$(command go env GOENV 2>/dev/null)\n if [[ -r $setup_go_env ]]; then\n while IFS= read -r setup_go_setting; do\n case $setup_go_setting in\n GOBIN=*) setup_go_bin=${setup_go_setting#GOBIN=} ;;\n esac\n done <\"$setup_go_env\"\n fi\n unset setup_go_env setup_go_setting\n fi\n if [[ -n $setup_go_bin ]]; then\n export GOBIN=\"$setup_go_bin\"\n path+=(\"$setup_go_bin\")\n else\n for setup_go in ${(s/:/)GOPATH}; do path+=(\"$setup_go/bin\"); done\n unset setup_go\n fi\n unset setup_go_bin\nfi\nfor setup_code in /Applications/'Visual Studio Code.app'/Contents/Resources/app/bin \"$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin\"; do\n [[ -x \"$setup_code/code\" ]] && path+=(\"$setup_code\")\ndone\nunset setup_code\nexport PATH\n", "font-casks.tsv": "font-meslo-lg-nerd-font\tMesloLGSNerdFont-Regular.ttf\nfont-jetbrains-mono-nerd-font\tJetBrainsMonoNerdFont-Regular.ttf\n", "formulae.txt": "git\ngit-lfs\ngh\ngo\nnode\npython\nuv\nripgrep\nduf\nfd\nbat\nfzf\nautojump\nzoxide\njq\nyq\ntmux\ntree\ngit-delta\ntig\nlazygit\nwget\nhtop\nshellcheck\nshfmt\nffmpeg\nimagemagick\nfastfetch\ncharmbracelet/tap/glow\ncharmbracelet/tap/pop\ncharmbracelet/tap/gum\ncharmbracelet/tap/crush\n", "git-defaults.json": "{\n \"alias.a\": \"add\",\n \"alias.aa\": \"add --all\",\n \"alias.ap\": \"add --patch\",\n \"alias.b\": \"branch\",\n \"alias.ba\": \"branch --all\",\n \"alias.bc\": \"!git symbolic-ref --quiet --short HEAD\",\n \"alias.bd\": \"branch --delete\",\n \"alias.c\": \"clone\",\n \"alias.ci\": \"commit\",\n \"alias.cia\": \"commit --all\",\n \"alias.ciaam\": \"commit --amend --all --message\",\n \"alias.ciam\": \"commit --all --message\",\n \"alias.cim\": \"commit --message\",\n \"alias.cl\": \"clone\",\n \"alias.co\": \"checkout\",\n \"alias.cob\": \"checkout -b\",\n \"alias.cof\": \"checkout --\",\n \"alias.d\": \"diff\",\n \"alias.ds\": \"diff --staged\",\n \"alias.dsw\": \"diff --staged --ignore-all-space\",\n \"alias.dw\": \"diff --ignore-all-space\",\n \"alias.f\": \"fetch\",\n \"alias.h\": \"help\",\n \"alias.i\": \"init\",\n \"alias.l\": \"log\",\n \"alias.la\": \"log --date=short --pretty=format:%h%x20%ad%x20-%x20[%an]%x20%s --graph\",\n \"alias.last\": \"log -1 HEAD --stat\",\n \"alias.lg\": \"log --graph --decorate --oneline --all\",\n \"alias.lr\": \"log --walk-reflogs\",\n \"alias.ls\": \"ls-files\",\n \"alias.lsg\": \"ls-files --others --ignored --exclude-standard\",\n \"alias.mg\": \"merge\",\n \"alias.mgn\": \"merge --no-ff\",\n \"alias.mgnm\": \"merge --no-ff --message\",\n \"alias.mgs\": \"merge --squash\",\n \"alias.mgt\": \"mergetool\",\n \"alias.pl\": \"pull\",\n \"alias.plo\": \"pull origin\",\n \"alias.ps\": \"push\",\n \"alias.ps1\": \"!f() { branch=$(git symbolic-ref --quiet --short HEAD) || exit; git push --set-upstream origin \\\"$branch\\\" \\\"$@\\\"; }; f\",\n \"alias.psa\": \"push --recurse-submodules=on-demand\",\n \"alias.psaa\": \"push --all\",\n \"alias.pso\": \"push origin\",\n \"alias.pst\": \"push --tags\",\n \"alias.rb\": \"rebase --committer-date-is-author-date\",\n \"alias.rba\": \"rebase --abort\",\n \"alias.rbc\": \"rebase --continue\",\n \"alias.rbi\": \"rebase --interactive --committer-date-is-author-date\",\n \"alias.rbs\": \"rebase --skip\",\n \"alias.rl\": \"reflog\",\n \"alias.rs\": \"reset\",\n \"alias.rsh\": \"reset --hard\",\n \"alias.rss\": \"reset --soft\",\n \"alias.rt\": \"remote\",\n \"alias.rtpo\": \"remote prune origin\",\n \"alias.s\": \"status --short\",\n \"alias.sh\": \"stash\",\n \"alias.size\": \"count-objects -vH\",\n \"alias.sm\": \"submodule\",\n \"alias.so\": \"show\",\n \"alias.sr\": \"remote show origin\",\n \"alias.st\": \"status --short --branch\",\n \"alias.stat\": \"!f() { echo '--- Branch ---'; git branch --show-current; echo; git status --short --branch; echo; echo '--- Changes (lines) ---'; git diff --stat HEAD; }; f\",\n \"alias.sts\": \"!f() { echo '=== Staged ==='; git diff --cached --numstat; echo; echo '=== Unstaged ==='; git diff --numstat; echo; echo '=== Untracked ==='; git ls-files --others --exclude-standard; }; f\",\n \"alias.sw\": \"switch\",\n \"alias.swc\": \"switch --create\",\n \"alias.t\": \"tag\",\n \"alias.ta\": \"tag --annotate\",\n \"alias.tig\": \"!tig\",\n \"alias.unstage\": \"restore --staged\",\n \"alias.v\": \"!git version\",\n \"branch.sort\": \"-committerdate\",\n \"color.ui\": \"auto\",\n \"commit.verbose\": \"true\",\n \"core.autocrlf\": \"input\",\n \"core.editor\": \"code --wait\",\n \"core.excludesFile\": \"~/.config/1bite/gitignore-global\",\n \"core.pager\": \"delta\",\n \"delta.line-numbers\": \"true\",\n \"delta.navigate\": \"true\",\n \"delta.side-by-side\": \"false\",\n \"diff.algorithm\": \"histogram\",\n \"diff.colorMoved\": \"default\",\n \"diff.tool\": \"vscode\",\n \"difftool.prompt\": \"false\",\n \"difftool.vscode.cmd\": \"code --wait --diff \\\"$LOCAL\\\" \\\"$REMOTE\\\"\",\n \"fetch.prune\": \"true\",\n \"help.autocorrect\": \"prompt\",\n \"init.defaultBranch\": \"main\",\n \"interactive.diffFilter\": \"delta --color-only\",\n \"merge.conflictStyle\": \"zdiff3\",\n \"merge.tool\": \"vscode\",\n \"mergetool.keepBackup\": \"false\",\n \"mergetool.vscode.cmd\": \"code --wait \\\"$MERGED\\\"\",\n \"pull.rebase\": \"true\",\n \"push.autoSetupRemote\": \"true\",\n \"push.default\": \"current\",\n \"push.followTags\": \"true\",\n \"rebase.autoStash\": \"true\",\n \"rerere.enabled\": \"true\",\n \"tag.sort\": \"-version:refname\"\n}\n", @@ -28,12 +28,12 @@ "vimrc": "\" Managed by 1bite. Personal overrides belong after the source line in ~/.vimrc.\nscriptencoding utf-8\nset encoding=utf-8\n\nset modelines=0\n\nset backspace=2\nset nocompatible\n\nset nobackup\nset nowritebackup\nset noswapfile\nset noundofile\n\nsyntax on\nset number\n\nset ruler\nset smartindent shiftwidth=4\nset tabstop=4\nset expandtab\n\nset list\nset listchars=tab:\u00bb\u00b7,trail:\u00b7,extends:>,precedes:<\n\ncom E e.\n\ncab W! w!\ncab Q! q!\ncab Wq wq\ncab Wa wa\ncab wQ wq\ncab WQ wq\ncab W w\ncab Q q\n", "vscode-extensions.txt": "golang.go\nms-python.python\ncharliermarsh.ruff\ndbaeumer.vscode-eslint\nesbenp.prettier-vscode\nredhat.vscode-yaml\ntimonwong.shellcheck\nms-vscode.sublime-keybindings\n", "vscode-settings.json": "{\n \"terminal.integrated.defaultProfile.osx\": \"zsh\",\n \"terminal.integrated.profiles.osx\": {\n \"zsh\": {\n \"path\": \"/bin/zsh\",\n \"args\": [\"-l\"]\n }\n },\n \"workbench.colorTheme\": \"Default Dark Modern\"\n}\n", - "zsh/development.zsh": "# Managed by 1bite: development. Personal overrides belong after the source in .zshrc.\nalias ll='ls -lah'\nalias g='git'\nalias cl='claude'\nalias clc='claude --continue'\n# Explicit opt-in shortcuts: skip Claude permission checks / Codex approvals and sandbox.\nalias cld='claude --dangerously-skip-permissions'\nalias cldc='claude --dangerously-skip-permissions --continue'\nalias cx='codex'\nalias cxc='codex resume --last'\nalias cxd='codex --dangerously-bypass-approvals-and-sandbox'\nalias cxdc='codex resume --last --dangerously-bypass-approvals-and-sandbox'\nalias dc='docker compose'\n\n# Portable local shortcuts; these override colliding Oh My Zsh aliases.\n\n# Make\nalias m=make\nalias mb='make build'\nalias mi='make install'\nalias mr='make run'\nalias mt='make test'\nalias mp='make preview'\n\n# Git\nalias ga='git add'\nalias gaa='git add --all'\nalias gcmsg='git commit --message'\nalias gs='git status -s'\nalias gst='git status'\nalias gss='git status --short'\nalias gd='git diff --no-index'\nalias gdiff='git diff'\nalias gds='git diff --staged'\nalias gdca='git diff --cached'\nalias gb='git branch'\nalias gba='git branch --all'\nalias gco='git checkout'\nalias gcb='git checkout -b'\nalias gsw='git switch'\nalias gswc='git switch --create'\nalias gl='git pull'\nalias gp='git push'\nalias glo='git log --oneline --decorate'\nalias glog='git log --oneline --decorate --graph'\nalias glg='git log --stat'\nalias gstl='git stash list'\nalias gstp='git stash pop'\nalias grs='git restore'\nalias grst='git restore --staged'\nalias git_undo_last='git reset --soft HEAD~1'\nalias gmtag='TZ=UTC git --no-pager show --quiet --abbrev=12 --date=\"format-local:%Y%m%d%H%M%S\" --format=\"v0.0.0-%cd-%h\"'\nalias lg='lazygit'\n\n# Go\nalias gdoc='go doc -all .'\nalias glist='go list -m -u all'\nalias glistj='go list -m -json all'\nalias grun='go run -v .'\nalias gbuild='go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildmac='GOOS=darwin GOARCH=arm64 go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildmacintel='GOOS=darwin GOARCH=amd64 go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildlinux='GOOS=linux GOARCH=amd64 go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildwindows='GOOS=windows GOARCH=amd64 go build -ldflags \"-s -w\" -trimpath -v .'\nfunction gcv {\n go test -v -race -cover -covermode=atomic -coverprofile=coverage.out -count 1 ./... \"$@\" || return\n go tool cover -html=coverage.out -o coverage.html\n}\nalias gtest='go test -v -race -cover -covermode=atomic -count 1 ./...'\nalias gbench='go test -parallel=4 -run=none -benchtime=2s -benchmem -bench=.'\nalias gm='go mod'\nalias gmi='go mod init'\nalias gmt='go mod tidy'\nalias gmg='go mod graph'\nalias gmc='go clean --modcache'\n\n# Docker; d stays the directory stack function defined below.\nalias dk=docker\nalias dkc='docker container'\nalias dkcm='docker compose'\nalias dexec='docker exec -it'\nalias di='docker images'\nalias dimg='docker images'\nalias dkimg='docker image ls'\nalias dklg='docker logs -f'\nalias dkls='docker ps -a'\nalias dkps='docker ps -a'\nalias dkrm='docker rm -f'\nalias dps='docker ps'\nalias dpsa='docker ps -a'\nalias drmi='docker rmi'\nalias dks='docker service'\nalias dksm='docker swarm'\nalias dkst='docker stack'\nalias dkstat='docker system df'\n\n# Disk\nalias df='df -h'\n\n# Multi-command shortcuts stop immediately if an earlier command fails.\nunalias gpre gps1 mkcd mcd d cdf o dl mktgz mkzip tfind ff jv jp jsonview pcat \\\n sha1 sha224 sha256 sha384 sha512 sha512224 sha512256 gci gcia git_corb \\\n git_ignore git_readme tn tad to tkss tmuxconf tds cn gcv dkclear fingerprint \\\n ffmpeg2wav ffmpeg2pcm video2wav pcm2wav heic2jpg png2jpg webp2png svg2png \\\n transpng img_trans img_pure_jpg img_pure_png new_bash 2>/dev/null || true\n", - "zsh/framework.zsh": "# Managed by 1bite: framework. Personal overrides belong after the source in .zshrc.\n# An already loaded Oh My Zsh/theme keeps control of the prompt.\nif (( ! $+functions[omz] )) && [[ -r \"${ZSH:-$HOME/.oh-my-zsh}/oh-my-zsh.sh\" ]]; then\n export ZSH=\"${ZSH:-$HOME/.oh-my-zsh}\"\n if (( ! ${+ZSH_THEME} )); then\n if [[ -r \"${ZSH_CUSTOM:-$ZSH/custom}/themes/powerlevel10k/powerlevel10k.zsh-theme\" ]]; then\n ZSH_THEME=powerlevel10k/powerlevel10k\n else\n ZSH_THEME=robbyrussell\n fi\n fi\n (( ${+plugins} )) || plugins=(git macos vscode web-search extract docker tmux)\n zstyle ':omz:update' mode disabled\n source \"$ZSH/oh-my-zsh.sh\"\nfi\n", + "zsh/development.zsh": "# Managed by 1bite: development. Personal overrides belong after the source in .zshrc.\nalias ll='ls -lah'\nalias g='git'\nif (( $+commands[claude] || $+functions[claude] )); then\n alias cl='claude'\n alias clc='claude --continue'\n # Explicit opt-in shortcuts: skip Claude permission checks.\n alias cld='claude --dangerously-skip-permissions'\n alias cldc='claude --dangerously-skip-permissions --continue'\nfi\nalias cx='codex'\nalias cxc='codex resume --last'\n# Explicit opt-in shortcuts: skip Codex approvals and sandbox.\nalias cxd='codex --dangerously-bypass-approvals-and-sandbox'\nalias cxdc='codex resume --last --dangerously-bypass-approvals-and-sandbox'\n\n# Portable local shortcuts; these override colliding Oh My Zsh aliases.\n\n# Make\nalias m=make\nalias mb='make build'\nalias mi='make install'\nalias mr='make run'\nalias mt='make test'\nalias mp='make preview'\n\n# Git\nalias ga='git add'\nalias gaa='git add --all'\nalias gcmsg='git commit --message'\nalias gs='git status -s'\nalias gst='git status'\nalias gss='git status --short'\nalias gd='git diff --no-index'\nalias gdiff='git diff'\nalias gds='git diff --staged'\nalias gdca='git diff --cached'\nalias gb='git branch'\nalias gba='git branch --all'\nalias gco='git checkout'\nalias gcb='git checkout -b'\nalias gsw='git switch'\nalias gswc='git switch --create'\nalias gl='git pull'\nalias gp='git push'\nalias glo='git log --oneline --decorate'\nalias glog='git log --oneline --decorate --graph'\nalias glg='git log --stat'\nalias gstl='git stash list'\nalias gstp='git stash pop'\nalias grs='git restore'\nalias grst='git restore --staged'\nalias git_undo_last='git reset --soft HEAD~1'\nalias gmtag='TZ=UTC git --no-pager show --quiet --abbrev=12 --date=\"format-local:%Y%m%d%H%M%S\" --format=\"v0.0.0-%cd-%h\"'\nalias lg='lazygit'\n\n# Go\nalias gdoc='go doc -all .'\nalias glist='go list -m -u all'\nalias glistj='go list -m -json all'\nalias grun='go run -v .'\nalias gbuild='go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildmac='GOOS=darwin GOARCH=arm64 go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildmacintel='GOOS=darwin GOARCH=amd64 go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildlinux='GOOS=linux GOARCH=amd64 go build -ldflags \"-s -w\" -trimpath -v .'\nalias gbuildwindows='GOOS=windows GOARCH=amd64 go build -ldflags \"-s -w\" -trimpath -v .'\nfunction gcv {\n go test -v -race -cover -covermode=atomic -coverprofile=coverage.out -count 1 ./... \"$@\" || return\n go tool cover -html=coverage.out -o coverage.html\n}\nalias gtest='go test -v -race -cover -covermode=atomic -count 1 ./...'\nalias gbench='go test -parallel=4 -run=none -benchtime=2s -benchmem -bench=.'\nalias gm='go mod'\nalias gmi='go mod init'\nalias gmt='go mod tidy'\nalias gmg='go mod graph'\nalias gmc='go clean --modcache'\n\n# Docker; d stays the directory stack function defined below.\nif (( $+commands[docker] || $+functions[docker] )); then\n alias dc='docker compose'\n alias dk=docker\n alias dkc='docker container'\n alias dkcm='docker compose'\n alias dexec='docker exec -it'\n alias di='docker images'\n alias dimg='docker images'\n alias dkimg='docker image ls'\n alias dklg='docker logs -f'\n alias dkls='docker ps -a'\n alias dkps='docker ps -a'\n alias dkrm='docker rm -f'\n alias dps='docker ps'\n alias dpsa='docker ps -a'\n alias drmi='docker rmi'\n alias dks='docker service'\n alias dksm='docker swarm'\n alias dkst='docker stack'\n alias dkstat='docker system df'\nfi\n\n# Disk\nalias df='df -h'\n\n# Multi-command shortcuts stop immediately if an earlier command fails.\nunalias gpre gps1 mkcd mcd d cdf o dl mktgz mkzip tfind ff jv jp jsonview pcat \\\n sha1 sha224 sha256 sha384 sha512 sha512224 sha512256 gci gcia git_corb \\\n git_ignore git_readme tn tad to tkss tmuxconf tds cn gcv fingerprint \\\n ffmpeg2wav ffmpeg2pcm video2wav pcm2wav heic2jpg png2jpg webp2png svg2png \\\n transpng img_trans img_pure_jpg img_pure_png new_bash 2>/dev/null || true\n", + "zsh/framework.zsh": "# Managed by 1bite: framework. Personal overrides belong after the source in .zshrc.\n# An already loaded Oh My Zsh/theme keeps control of the prompt.\nif (( ! $+functions[omz] )) && [[ -r \"${ZSH:-$HOME/.oh-my-zsh}/oh-my-zsh.sh\" ]]; then\n export ZSH=\"${ZSH:-$HOME/.oh-my-zsh}\"\n if (( ! ${+ZSH_THEME} )); then\n if [[ -r \"${ZSH_CUSTOM:-$ZSH/custom}/themes/powerlevel10k/powerlevel10k.zsh-theme\" ]]; then\n ZSH_THEME=powerlevel10k/powerlevel10k\n else\n ZSH_THEME=robbyrussell\n fi\n fi\n if (( ! ${+plugins} )); then\n plugins=(git macos vscode web-search extract)\n (( $+commands[docker] )) && plugins+=(docker)\n plugins+=(tmux)\n fi\n zstyle ':omz:update' mode disabled\n source \"$ZSH/oh-my-zsh.sh\"\nfi\n", "zsh/git-functions.zsh": "# Managed by 1bite: Git functions. Personal overrides belong after the source in .zshrc.\n# Git helpers with validation; these replace the plain gci/gcia aliases.\nunalias gci gcia 2>/dev/null || true\nfunction gci {\n if [[ -z $1 ]]; then echo \"missing commit message\" >&2; return 1; fi\n git commit -a -m \"$*\"\n}\nfunction gcia {\n if [[ -z $1 ]]; then echo \"missing commit message\" >&2; return 1; fi\n git commit --amend -a -m \"$*\"\n}\nalias dif='git diff --no-index'\nalias gmd='git checkout master && git pull && git remote prune origin'\nfunction git_corb {\n if [[ -z $1 ]]; then\n echo \"missing remote branch name for 'git checkout remote branch'\" >&2\n return 1\n fi\n git checkout -b \"$1\" origin/\"$1\"\n}\nfunction git_ignore {\n local name=.gitignore\n if [[ -z $1 ]]; then\n local curdir=\"$(pwd)\"\n echo \"/${curdir##*/}\" >>\"$name\"\n else\n echo \"$1\" >>\"$name\"\n fi\n git add \"$name\"\n}\nfunction git_readme {\n local name=README.md\n if [[ -z $1 ]]; then\n local curdir=\"$(pwd)\"\n echo \"# ${curdir##*/}\" >>\"$name\"\n else\n echo \"$1\" >>\"$name\"\n fi\n git add \"$name\"\n}\n", "zsh/media.zsh": "# Managed by 1bite: media helpers. Personal overrides belong after the source in .zshrc.\n# Media conversion helpers use tools already declared by the installer.\nfunction ffmpeg2wav {\n (( $# )) || { print -u2 'usage: ffmpeg2wav [...]'; return 1; }\n local file target\n for file in \"$@\"; do\n [[ -f $file ]] || { print -u2 \"not a file: $file\"; return 1; }\n target=\"${file:r}.wav\"\n command ffmpeg -nostdin -hide_banner -loglevel warning -n -i \"$file\" \\\n -acodec pcm_s16le -ac 1 -ar 16000 -f wav \"$target\" || return\n done\n}\nfunction video2wav { ffmpeg2wav \"$@\"; }\nfunction ffmpeg2pcm {\n (( $# )) || { print -u2 'usage: ffmpeg2pcm [...]'; return 1; }\n local file target\n for file in \"$@\"; do\n [[ -f $file ]] || { print -u2 \"not a file: $file\"; return 1; }\n target=\"${file:r}.pcm\"\n command ffmpeg -nostdin -hide_banner -loglevel warning -n -i \"$file\" \\\n -acodec pcm_s16le -ac 1 -ar 16000 -f s16le \"$target\" || return\n done\n}\nfunction pcm2wav {\n (( $# )) || { print -u2 'usage: pcm2wav <16-bit-16k-mono-pcm> [...]'; return 1; }\n local file target\n for file in \"$@\"; do\n [[ -f $file ]] || { print -u2 \"not a file: $file\"; return 1; }\n target=\"${file:r}.wav\"\n command ffmpeg -nostdin -hide_banner -loglevel warning -n -f s16le -ar 16000 -ac 1 \\\n -i \"$file\" \"$target\" || return\n done\n}\nfunction heic2jpg {\n (( $# )) || { print -u2 'usage: heic2jpg [...]'; return 1; }\n local file\n for file in \"$@\"; do\n [[ -f $file && ${file:e:l} == heic ]] || { print -u2 \"not a HEIC file: $file\"; return 1; }\n command sips -s format jpeg \"$file\" --out \"${file:r}.jpg\" >/dev/null || return\n done\n}\nfunction png2jpg {\n (( $# )) || { print -u2 'usage: png2jpg [...]'; return 1; }\n local file\n for file in \"$@\"; do\n [[ -f $file && ${file:e:l} == png ]] || { print -u2 \"not a PNG file: $file\"; return 1; }\n command magick \"$file\" -background white -alpha remove -alpha off -quality 96 \"${file:r}.jpg\" || return\n done\n}\nfunction webp2png {\n (( $# )) || { print -u2 'usage: webp2png [...]'; return 1; }\n local file\n for file in \"$@\"; do\n [[ -f $file && ${file:e:l} == webp ]] || { print -u2 \"not a WebP file: $file\"; return 1; }\n command magick \"$file\" \"${file:r}.png\" || return\n done\n}\nfunction svg2png {\n (( $# )) || { print -u2 'usage: svg2png [...]'; return 1; }\n local file\n for file in \"$@\"; do\n [[ -f $file && ${file:e:l} == svg ]] || { print -u2 \"not an SVG file: $file\"; return 1; }\n command magick -background none \"$file\" \"${file:r}.png\" || return\n done\n}\nfunction transpng {\n (( $# )) || { print -u2 'usage: transpng [...]'; return 1; }\n local file\n for file in \"$@\"; do\n [[ -f $file ]] || { print -u2 \"not a file: $file\"; return 1; }\n command magick \"$file\" -fuzz 10% -transparent white \"${file:r}-trans.png\" || return\n done\n}\nfunction img_trans {\n local size=${1:-500}\n [[ $size == <1-> ]] || { print -u2 'usage: img_trans [positive-size]'; return 1; }\n command magick -size \"${size}x${size}\" xc:transparent \"PNG32:trans-${size}.png\"\n}\nfunction img_pure_jpg {\n local size=${1:-500} color=${2:-white}\n [[ $size == <1-> ]] || { print -u2 'usage: img_pure_jpg [positive-size] [color]'; return 1; }\n command magick -size \"${size}x${size}\" \"xc:${color}\" -quality 100 \"pure-${size}.jpg\"\n}\nfunction img_pure_png {\n local size=${1:-500} color=${2:-white}\n [[ $size == <1-> ]] || { print -u2 'usage: img_pure_png [positive-size] [color]'; return 1; }\n command magick -size \"${size}x${size}\" \"xc:${color}\" \"PNG32:pure-${size}.png\"\n}\n\nfunction new_bash {\n local target=${1:-test.sh}\n [[ ! -e $target ]] || { print -u2 \"refusing to overwrite: $target\"; return 1; }\n cat >\"$target\" <<'ONE_BITE_BASH_TEMPLATE'\n#!/bin/bash\nset -euo pipefail\n\nSCRIPT_DIR=$(cd -P -- \"$(dirname -- \"${BASH_SOURCE[0]}\")\" && pwd -P)\ncd \"$SCRIPT_DIR\"\nONE_BITE_BASH_TEMPLATE\n chmod u+x \"$target\"\n}\n", "zsh/options.zsh": "# Managed by 1bite: options. Personal overrides belong after the source in .zshrc.\nbindkey -e\nbindkey '^[[1;5D' backward-word\nbindkey '^[[1;5C' forward-word\nbindkey '^[[1;3D' backward-word\nbindkey '^[[1;3C' forward-word\nbindkey '^[b' backward-word\nbindkey '^[f' forward-word\nbindkey '^A' beginning-of-line\nbindkey '^E' end-of-line\nbindkey '^[[H' beginning-of-line\nbindkey '^[[F' end-of-line\nbindkey '^[[3~' delete-char\nbindkey '^W' backward-kill-word\nbindkey '^[^?' backward-kill-word\n\nHISTFILE=\"$HOME/.zsh_history\"\nHISTSIZE=50000\nSAVEHIST=50000\nsetopt append_history hist_ignore_dups share_history\n", - "zsh/terminal.zsh": "# Managed by 1bite: terminal. Personal overrides belong after the source in .zshrc.\n# Editor and paths\nfunction cn {\n if (( $# )); then code -n \"$@\"; else code -n .; fi\n}\nalias cdiff='code -n --diff'\nalias rp=realpath\n\n# tmux: retain local ts=list and tn=new conventions alongside OMZ shortcuts.\nalias t=tmux\nalias ts='tmux ls'\nalias ta='tmux attach -t'\nalias tk='tmux kill-session -t'\nfunction tn {\n if [[ -z $1 ]]; then\n tmux new-session\n else\n tmux new-session -s \"$1\"\n fi\n}\nfor ((setup_tmux_index = 0; setup_tmux_index <= 16; setup_tmux_index++)); do\n alias ta$setup_tmux_index=\"tmux attach -t $setup_tmux_index\"\ndone\nunset setup_tmux_index\nalias tl='tmux list-sessions'\nalias tksv='tmux kill-server'\nfunction tad {\n if [[ -z $1 || $1 == -* ]]; then tmux attach -d \"$@\"; else tmux attach -d -t \"$@\"; fi\n}\nfunction to {\n if [[ -z $1 || $1 == -* ]]; then tmux new-session -A \"$@\"; else tmux new-session -A -s \"$@\"; fi\n}\nfunction tkss {\n if [[ -z $1 || $1 == -* ]]; then tmux kill-session \"$@\"; else tmux kill-session -t \"$@\"; fi\n}\nfunction tmuxconf {\n local setup_tmux_config=${ZSH_TMUX_CONFIG:-}\n if [[ -z $setup_tmux_config ]]; then\n if [[ -f \"$HOME/.tmux.conf\" ]]; then\n setup_tmux_config=\"$HOME/.tmux.conf\"\n elif [[ -f \"${XDG_CONFIG_HOME:-$HOME/.config}/tmux/tmux.conf\" ]]; then\n setup_tmux_config=\"${XDG_CONFIG_HOME:-$HOME/.config}/tmux/tmux.conf\"\n else\n setup_tmux_config=\"$HOME/.tmux.conf\"\n fi\n fi\n local -a setup_editor\n setup_editor=(${(z)${EDITOR:-vi}})\n \"${(@Q)setup_editor}\" \"$setup_tmux_config\"\n}\nfunction tds {\n local setup_tmux_hash setup_tmux_name\n if (( $+commands[md5] )); then\n setup_tmux_hash=$(printf '%s' \"$PWD\" | command md5 -q) || return\n else\n setup_tmux_hash=$(printf '%s' \"$PWD\" | command md5sum) || return\n fi\n setup_tmux_name=\"${PWD:t}\"\n setup_tmux_name=\"${setup_tmux_name//[.:]/_}\"\n tmux new-session -A -s \"${setup_tmux_name:-root}-${setup_tmux_hash[1,6]}\" \"$@\"\n}\n\n# Docker cleanup\nfunction dkclear {\n docker system prune -f\n}\n\n# Misc\nalias reload='. ~/.zshrc'\nalias cls='clear'\nalias e='exit'\nalias ns=nslookup\nalias weather='curl wttr.in'\nalias webserver='python3 -m http.server'\nfunction fingerprint {\n if [[ -z $1 ]]; then\n echo \"missing target ssh key\" >&2\n return 1\n fi\n ssh-keygen -E md5 -lf \"$1\" && ssh-keygen -E sha256 -lf \"$1\"\n}\n", + "zsh/terminal.zsh": "# Managed by 1bite: terminal. Personal overrides belong after the source in .zshrc.\n# Editor and paths\nfunction cn {\n if (( $# )); then code -n \"$@\"; else code -n .; fi\n}\nalias cdiff='code -n --diff'\nalias rp=realpath\n\n# tmux: retain local ts=list and tn=new conventions alongside OMZ shortcuts.\nalias t=tmux\nalias ts='tmux ls'\nalias ta='tmux attach -t'\nalias tk='tmux kill-session -t'\nfunction tn {\n if [[ -z $1 ]]; then\n tmux new-session\n else\n tmux new-session -s \"$1\"\n fi\n}\nfor ((setup_tmux_index = 0; setup_tmux_index <= 16; setup_tmux_index++)); do\n alias ta$setup_tmux_index=\"tmux attach -t $setup_tmux_index\"\ndone\nunset setup_tmux_index\nalias tl='tmux list-sessions'\nalias tksv='tmux kill-server'\nfunction tad {\n if [[ -z $1 || $1 == -* ]]; then tmux attach -d \"$@\"; else tmux attach -d -t \"$@\"; fi\n}\nfunction to {\n if [[ -z $1 || $1 == -* ]]; then tmux new-session -A \"$@\"; else tmux new-session -A -s \"$@\"; fi\n}\nfunction tkss {\n if [[ -z $1 || $1 == -* ]]; then tmux kill-session \"$@\"; else tmux kill-session -t \"$@\"; fi\n}\nfunction tmuxconf {\n local setup_tmux_config=${ZSH_TMUX_CONFIG:-}\n if [[ -z $setup_tmux_config ]]; then\n if [[ -f \"$HOME/.tmux.conf\" ]]; then\n setup_tmux_config=\"$HOME/.tmux.conf\"\n elif [[ -f \"${XDG_CONFIG_HOME:-$HOME/.config}/tmux/tmux.conf\" ]]; then\n setup_tmux_config=\"${XDG_CONFIG_HOME:-$HOME/.config}/tmux/tmux.conf\"\n else\n setup_tmux_config=\"$HOME/.tmux.conf\"\n fi\n fi\n local -a setup_editor\n setup_editor=(${(z)${EDITOR:-vi}})\n \"${(@Q)setup_editor}\" \"$setup_tmux_config\"\n}\nfunction tds {\n local setup_tmux_hash setup_tmux_name\n if (( $+commands[md5] )); then\n setup_tmux_hash=$(printf '%s' \"$PWD\" | command md5 -q) || return\n else\n setup_tmux_hash=$(printf '%s' \"$PWD\" | command md5sum) || return\n fi\n setup_tmux_name=\"${PWD:t}\"\n setup_tmux_name=\"${setup_tmux_name//[.:]/_}\"\n tmux new-session -A -s \"${setup_tmux_name:-root}-${setup_tmux_hash[1,6]}\" \"$@\"\n}\n\n# Docker cleanup\nif (( $+commands[docker] || $+functions[docker] )); then\n unalias dkclear 2>/dev/null || true\n function dkclear {\n docker system prune -f\n }\nfi\n\n# Misc\nalias reload='. ~/.zshrc'\nalias cls='clear'\nalias e='exit'\nalias ns=nslookup\nalias weather='curl wttr.in'\nalias webserver='python3 -m http.server'\nfunction fingerprint {\n if [[ -z $1 ]]; then\n echo \"missing target ssh key\" >&2\n return 1\n fi\n ssh-keygen -E md5 -lf \"$1\" && ssh-keygen -E sha256 -lf \"$1\"\n}\n", "zsh/tools.zsh": "# Managed by 1bite: tools. Personal overrides belong after the source in .zshrc.\n# Native AutoJump owns j; zoxide retains its separate z / zi commands and database.\nif (( $+commands[autojump] )); then\n setup_autojump=\"${commands[autojump]:A:h:h}/share/autojump/autojump.zsh\"\n if [[ -r \"$setup_autojump\" ]]; then\n # Preserve personal functions, aliases and commands, and load the hook only once.\n if (( ! $+functions[j] && ! $+aliases[j] && ! $+commands[j] )); then\n () {\n setopt localoptions noaliases\n source \"$1\"\n } \"$setup_autojump\"\n fi\n fi\n unset setup_autojump\nfi\nif command -v zoxide >/dev/null; then\n setup_zoxide_init=$(zoxide init zsh) || return\n # Current zoxide ends its interactive completion block with a false feature\n # check when compdef is unavailable. That is a valid initialization result,\n # so append a successful no-op without hiding generation or syntax failures.\n eval \"$setup_zoxide_init\"$'\\n:' || return\n unset setup_zoxide_init\nfi\nif [[ -t 0 && -t 1 ]] && (( $+commands[fzf] )); then\n if setup_fzf_init=$(command fzf --zsh 2>/dev/null) && [[ -n $setup_fzf_init ]]; then\n eval \"$setup_fzf_init\" || return\n else\n # fzf before 0.48 ships the same integration as files instead of --zsh.\n setup_fzf_shell=\"${commands[fzf]:A:h:h}/shell\"\n [[ -r \"$setup_fzf_shell/completion.zsh\" ]] && source \"$setup_fzf_shell/completion.zsh\"\n [[ -r \"$setup_fzf_shell/key-bindings.zsh\" ]] && source \"$setup_fzf_shell/key-bindings.zsh\"\n fi\n unset setup_fzf_init setup_fzf_shell\nfi\n", "zsh/utilities.zsh": "# Managed by 1bite: general utilities. Personal overrides belong after the source in .zshrc.\nfunction gpre { git status && git add --all && git diff --staged -w \"$@\"; }\nfunction gps1 {\n local branch\n branch=$(git symbolic-ref --quiet --short HEAD) || return\n git push --set-upstream origin \"$branch\" \"$@\"\n}\n\nfunction mkcd { [[ $# == 1 && -n $1 ]] || { print -u2 \"usage: mkcd \"; return 1; }; mkdir -p -- \"$1\" && cd -- \"$1\"; }\nfunction mcd { mkcd \"$@\"; }\n\n# Directory stack, Oh My Zsh compatible; redefined here so it also works without OMZ.\nunalias d 2>/dev/null || true\nfunction d {\n if [[ -n $1 ]]; then\n dirs \"$@\"\n else\n dirs -v | head -n 10\n fi\n}\n\n# Files and directories\nfunction cdf {\n if [[ -d $1 ]]; then\n cd -- \"$1\"\n else\n cd -- \"$(dirname \"$1\")\"\n fi\n}\nfunction o {\n if [[ $# -lt 1 ]]; then\n open .\n else\n open \"$1\"\n fi\n}\nfunction dl {\n case $# in\n 2) curl --fail --location --output \"$2\" -- \"$1\" ;;\n 1) curl --fail --location --remote-name -- \"$1\" ;;\n *) echo \"usage: dl [output]\" >&2; return 1 ;;\n esac\n}\nfunction mktgz {\n if [[ -z $1 ]]; then echo \"missing source target\" >&2; return 1; fi\n tar cvzf \"${1%%/}.tgz\" \"${1%%/}/\"\n}\nfunction mkzip {\n if [[ -z $1 ]]; then echo \"missing source target\" >&2; return 1; fi\n zip -r -8 \"${1%%/}.zip\" \"$1\"\n}\nfunction tfind {\n if [[ -z $1 ]]; then echo \"missing target keyword\" >&2; return 1; fi\n echo \"Searching for '$*'\"\n rg --ignore-case --fixed-strings --glob '*.txt' --glob '*.md' -- \"$*\" .\n}\nfunction ff {\n setopt localoptions pipefail\n local target=${1:-.}\n [[ -d $target ]] || { print -u2 \"not a directory: $target\"; return 1; }\n command du -k -d 1 -- \"$target\" 2>/dev/null | command sort -n | command awk '\n function human(k) {\n if (k >= 1073741824) return sprintf(\"%.1fT\", k / 1073741824)\n if (k >= 1048576) return sprintf(\"%.1fG\", k / 1048576)\n if (k >= 1024) return sprintf(\"%.1fM\", k / 1024)\n return sprintf(\"%dK\", k)\n }\n { size=$1; sub(/^[^[:space:]]+[[:space:]]+/, \"\"); printf \"%8s %s\\n\", human(size), $0 }\n '\n}\nalias trim=\"awk '{\\$1=\\$1;print}'\"\nalias lsp=\"find . -type f -not -path '*/\\.git/*' | sed 's/^\\.\\///g' | sort\"\nalias lsmax=\"find . -type f -not -path '*/\\.git/*' -print0 | xargs -0r stat -f '%z %N' | sort -nr | head -10\"\nalias lslast=\"find . -type f -not -path '*/\\.git/*' -print0 | xargs -0r stat -f '%Sm %N' -t '%Y-%m-%d %T' | sort -nr | head -10\"\n\n# Viewing (jq, bat, glow)\nfunction jv { jq <\"$1\" -C . | less -R; }\nfunction jp { jq -M . \"$1\"; }\nfunction jsonview { printf '%s\\n' \"$*\" | jq -C -r .; }\nalias ccat='bat --paging=never'\nalias mcat='glow'\nalias readme='glow README.md'\n\n# Clipboard and clock; now uses local time, utcnow uses UTC.\nalias ppwd='pwd | pbcopy ; pbpaste'\nalias pd='basename \"$PWD\" | pbcopy ; pbpaste'\nalias pc=pbcopy\nalias pp=pbpaste\nalias l2l='pbpaste | paste -sd \" \" - | pbcopy'\nalias now='date \"+%Y-%m-%d %H:%M:%S %Z\" | pbcopy ; pbpaste'\nalias utcnow='TZ=UTC date \"+%Y-%m-%d %H:%M:%S %Z\" | pbcopy ; pbpaste'\nfunction pcat {\n if [[ -z $1 ]]; then echo \"missing source target\" >&2; return 1; fi\n cat -- \"$@\" | pbcopy\n}\n\n# Hashes and encoding\nfunction sha1 { shasum -a 1 \"$@\"; }\nfunction sha224 { shasum -a 224 \"$@\"; }\nfunction sha256 { shasum -a 256 \"$@\"; }\nfunction sha384 { shasum -a 384 \"$@\"; }\nfunction sha512 { shasum -a 512 \"$@\"; }\nfunction sha512224 { shasum -a 512224 \"$@\"; }\nfunction sha512256 { shasum -a 512256 \"$@\"; }\nalias b64e='base64'\nalias b64d='base64 -d'\n" } diff --git a/tests/test_editor.py b/tests/test_editor.py index 505c85a..98389df 100644 --- a/tests/test_editor.py +++ b/tests/test_editor.py @@ -278,13 +278,13 @@ def test_all_helper_function_alias_collisions_and_reload(self): 'ffmpeg2wav ffmpeg2pcm video2wav pcm2wav heic2jpg png2jpg webp2png svg2png ' 'transpng img_trans img_pure_jpg img_pure_png new_bash').split() prefix = '\n'.join(f"alias {name}='print WRONG'" for name in names) - result = self.zsh(prefix + '\nsource "$1"\nsource "$1"\nwhence -w ' + ' '.join(names)) + result = self.zsh(prefix + '\ndocker() { :; }\nsource "$1"\nsource "$1"\nwhence -w ' + ' '.join(names)) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), [name + ': function' for name in names]) def test_imported_helpers_failure_codes_literal_search_and_json(self): for code in (0, 23): - result = self.zsh('source "$1"\ndocker() { print -r -- "$*"; return ' + str(code) + '; }\ndkclear') + result = self.zsh('docker() { print -r -- "$*"; return ' + str(code) + '; }\nsource "$1"\ndkclear') self.assertEqual(result.returncode, code, result.stderr) self.assertEqual(result.stdout.splitlines(), ['system prune -f']) result = self.zsh('source "$1"\ncurl() { return 22; }\ndl https://example.invalid/file') @@ -633,6 +633,9 @@ def test_entire_extension_iteration_survives_stdin_consumers(self): def test_real_zsh_default_existing_and_empty_themes_and_noninteractive(self): self.fake_omz() + docker = self.home / '.local/bin/docker' + docker.write_text('#!/bin/sh\nexit 0\n') + docker.chmod(0o755) defaults = 'git macos vscode web-search extract docker tmux' for before, expected in [('', f'powerlevel10k/powerlevel10k:{defaults}:1'), ('ZSH_THEME=custom; plugins=(git python);', 'custom:git python:1'), @@ -649,6 +652,36 @@ def test_real_zsh_default_existing_and_empty_themes_and_noninteractive(self): self.assertEqual(result.stdout.strip(), 'robbyrussell') self.assertEqual(result.returncode, 0, result.stderr) + def test_optional_plugins_aliases_and_functions_follow_available_clients(self): + self.fake_omz() + script = r''' +path=("$HOME/empty-bin") +alias cl=personal-claude +alias dk=personal-docker +alias dkclear=personal-clear +source "${1:h}/zsh/framework.zsh" +source "${1:h}/zsh/development.zsh" +source "${1:h}/zsh/terminal.zsh" +[[ "${plugins[*]}" == 'git macos vscode web-search extract tmux' ]] || exit 11 +[[ "$aliases[cl]" == personal-claude && "$aliases[dk]" == personal-docker && "$aliases[dkclear]" == personal-clear ]] || exit 12 +for name in clc cld cldc dc dkc dkcm dexec di dimg dkimg dklg dkls dkps dkrm dps dpsa drmi dks dksm dkst dkstat; do + (( ! $+aliases[$name] )) || exit 13 +done +(( ! $+functions[dkclear] )) || exit 14 +''' + result = self.zsh(script) + self.assertEqual(result.returncode, 0, result.stderr) + + result = self.zsh(r''' +path=("$HOME/empty-bin") +claude() { :; } +docker() { :; } +source "${1:h}/zsh/development.zsh" +source "${1:h}/zsh/terminal.zsh" +[[ "$aliases[cl]" == claude && "$aliases[dk]" == docker && $+functions[dkclear] -eq 1 ]] +''') + self.assertEqual(result.returncode, 0, result.stderr) + def test_real_zsh_environment_paths_deduplicate_and_keep_overrides(self): for assignments, expected in [('GOBIN="$HOME/custom bin"; GOPATH="$HOME/work";', [self.home / 'custom bin']), ('unset GOBIN; GOPATH="$HOME/a:$HOME/b";', [self.home / 'a/bin', self.home / 'b/bin']), @@ -666,6 +699,8 @@ def test_real_zsh_environment_paths_deduplicate_and_keep_overrides(self): self.assertIn(str(path), entries) for path in expected: self.assertGreater(entries.index(str(path)), entries.index(str(self.home / '.local/bin'))) + docker_bin = Path('/Applications/Docker.app/Contents/Resources/bin') + self.assertEqual(str(docker_bin) in entries, os.access(docker_bin / 'docker', os.X_OK)) def test_docker_default_platform_is_global_and_can_be_overridden(self): result = self.zsh('unset DOCKER_DEFAULT_PLATFORM; source "$1"; print -r -- "$DOCKER_DEFAULT_PLATFORM"', diff --git a/tests/test_setup.py b/tests/test_setup.py index 844c552..fa35d26 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -305,6 +305,50 @@ def test_duplicate_recognized_shell_loaders_collapse_but_similar_personal_lines_ self.assertEqual(rows.count(configure.SOURCE_LINE), 1) self.assertIn(personal.strip(), rows) + def test_dangling_docker_completions_are_repaired_without_changing_p10k(self): + prefix = self.home / 'brew' + completions = prefix / 'share/zsh/site-functions' + completions.mkdir(parents=True) + targets = { + '_docker': Path(os.path.realpath(self.home / 'missing-docker-completion')), + '_docker-compose': Path(os.path.realpath(self.home / 'missing-docker-compose-completion')), + } + docker = completions / '_docker' + compose = completions / '_docker-compose' + docker.symlink_to(targets['_docker']) + compose.symlink_to(targets['_docker-compose']) + p10k = self.home / '.p10k.zsh' + personal = "typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(status disk_usage time)\n" + p10k.write_text(personal) + + with mock.patch.object(configure, 'DOCKER_ZSH_COMPLETIONS', targets), \ + mock.patch.dict(os.environ, {'HOMEBREW_PREFIX': str(prefix)}): + with self.assertRaisesRegex(ValueError, 'compinit startup output'): + configure.verify_zsh_completions() + output = io.StringIO() + with contextlib.redirect_stdout(output): + configure.configure(self.home, ROOT / 'config', with_claude=True) + configure.verify(self.home, with_claude=True) + + self.assertFalse(docker.is_symlink()) + self.assertFalse(compose.is_symlink()) + self.assertEqual(p10k.read_text(), personal) + self.assertEqual(output.getvalue().count('removed broken Docker Desktop completion link'), 2) + + def test_unknown_dangling_completion_is_preserved_and_stops_before_config_writes(self): + prefix = self.home / 'brew' + completions = prefix / 'share/zsh/site-functions' + completions.mkdir(parents=True) + unknown = completions / '_personal' + unknown.symlink_to(self.home / 'missing-personal-completion') + + with mock.patch.dict(os.environ, {'HOMEBREW_PREFIX': str(prefix)}): + with self.assertRaisesRegex(ValueError, 'not owned by One Bite'): + self.apply() + + self.assertTrue(unknown.is_symlink()) + self.assertFalse((self.home / '.config/1bite').exists()) + def test_vim_template_loads_core_defaults(self): executable = shutil.which('vim') if not executable: