Bash Script Installer for Zsh
The "Bash Script Installer" simplifies the setup of Zsh.
Zsh
Ubuntu
bash -c "$(curl -fsSL https://tinyurl.com/y6fhb594)"
WSL
bash -c "$(curl -fsSL https://tinyurl.com/ycyct6wp)"
MacOS
bash -c "$(curl -fsSL https://tinyurl.com/yptybtpa)"
Configuration
bash -c "$(curl -fsSL https://tinyurl.com/y8vf753j)"
Shell framework and prompt
The installers use Oh My Zsh for framework features and plugins, with the OMZ theme disabled:
ZSH_THEME=""
source "$ZSH/oh-my-zsh.sh"
eval "$(starship init zsh)"
Starship owns the prompt and loads after Oh My Zsh. The managed Starship config is installed at ~/.config/starship.toml. A Nerd Font is required for the Powerline symbols.
Plan-build workflow
The local plan_build function is a thin cached launcher for the canonical public plan-build gist. When curl is available, each invocation stages plan_build.zsh, SKILL.md, and ARCHITECT.md, requires all three to be non-empty, and syntax-checks the executable. It then moves the complete bundle into a unique immutable release directory and atomically switches one current symlink. Refresh activation is serialized with a kernel-backed Zsh file lock so concurrent callers cannot mix releases and crashes cannot leave a stale lock. If refresh fails, the launcher preserves and uses the last complete, validated release and repairs its skill links. Zsh is always required; curl is required only when no valid cached release exists.
plan_build
plan_build --prompt
plan_build --writing-plan
plan_build --prompt --brainstorm
plan_build --yolo --writing-plan
plan_build --architect
plan_build --architect --new
plan_build --architect --yolo
The canonical executable defines the current command behavior and options. Each invocation binds the executable and both skills to its selected immutable release. The launcher also points the installed Claude Code skill paths through the active current release:
SKILL.md→~/.claude/skills/plan-build/SKILL.mdARCHITECT.md→~/.claude/skills/plan-build-architect/SKILL.md
The launcher defaults to https://opengist.resetrix.work/weehong/plan-build/raw/HEAD and ${XDG_CACHE_HOME:-$HOME/.cache}/plan-build. Override these with PLAN_BUILD_BASE_URL and PLAN_BUILD_CACHE_DIR. Installed skill paths remain fixed at ~/.claude/skills; cached invocations receive immutable release paths through environment variables. The downloaded script is remote executable code: syntax validation catches malformed Zsh but is not a security sandbox, so only point PLAN_BUILD_BASE_URL at a source you trust. Remote code is executed in a child Zsh process and is never sourced into the caller shell.
Invoking plan_build once installs or repairs both skill links. To install standalone files without the cached launcher, follow the installation instructions in the canonical gist instead.
| 1 | # ============================================================================= |
| 2 | # OS-SPECIFIC ALIASES |
| 3 | # ============================================================================= |
| 4 | if [[ "$OSTYPE" == "linux-gnu"* ]] && command -v apt-get >/dev/null 2>&1; then |
| 5 | # Linux Only System update & cleanup |
| 6 | alias uu='sudo apt-get update && \ |
| 7 | sudo apt-get upgrade -y && \ |
| 8 | sudo apt-get full-upgrade -y && \ |
| 9 | sudo apt-get autoremove -y && \ |
| 10 | sudo apt-get autoclean -y && \ |
| 11 | sudo apt-get clean' |
| 12 | elif [[ "$OSTYPE" == "darwin"* ]]; then |
| 13 | # macOS Only |
| 14 | alias flushdns='sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder' |
| 15 | fi |
| 16 | |
| 17 | # ============================================================================= |
| 18 | # UNIVERSAL GIT ALIASES (Fixed: Removed broken "$@" from aliases) |
| 19 | # ============================================================================= |
| 20 | alias gph='git push' |
| 21 | alias gco='git checkout' |
| 22 | alias gbh='git branch' |
| 23 | alias gmt='git commit' |
| 24 | alias gpl='git pull' |
| 25 | alias grb='git rebase' |
| 26 | alias grt='git reset' |
| 27 | alias gst='git status' |
| 28 | alias grmrf='git checkout -- . && git clean -fd' |
| 1 | #!/bin/bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # Configuration |
| 5 | GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD" |
| 6 | CONFIG_FILES=( |
| 7 | ".alias" |
| 8 | ".func" |
| 9 | ".pathrc" |
| 10 | ".sourcerc" |
| 11 | ".vimrc" |
| 12 | ".zshrc" |
| 13 | ".config/starship.toml" |
| 14 | ) |
| 15 | |
| 16 | echo "Starting configuration download..." |
| 17 | download_failed=0 |
| 18 | |
| 19 | for f in "${CONFIG_FILES[@]}"; do |
| 20 | # Remove the leading dot for the URL path |
| 21 | remote_name="${f#.}" |
| 22 | [[ "$f" == ".config/starship.toml" ]] && remote_name="starship.toml" |
| 23 | url="$GIST_RAW_BASE/$remote_name" |
| 24 | target="$HOME/$f" |
| 25 | tmp="${target}.tmp.$$" |
| 26 | |
| 27 | echo "Downloading $f..." |
| 28 | mkdir -p "$(dirname "$target")" |
| 29 | |
| 30 | # Use -f to fail silently on server errors, -s for silent, -L to follow redirects |
| 31 | if curl -fsSL "$url" -o "$tmp" && mv "$tmp" "$target"; then |
| 32 | echo "Successfully updated $target" |
| 33 | else |
| 34 | rm -f "$tmp" |
| 35 | echo "Error: Failed to download $f from $url" >&2 |
| 36 | download_failed=1 |
| 37 | fi |
| 38 | done |
| 39 | |
| 40 | if [ "$download_failed" -ne 0 ]; then |
| 41 | echo "Configuration download completed with errors." >&2 |
| 42 | exit 1 |
| 43 | fi |
| 44 | |
| 45 | echo "Done! All configuration files have been replaced." |
| 46 |
| 1 | # ============================================================================= |
| 2 | # CUSTOM FUNCTIONS |
| 3 | # ============================================================================= |
| 4 | |
| 5 | # ----------------------------------------------------------------------------- |
| 6 | # Function: funcs (List available .func functions) |
| 7 | # ----------------------------------------------------------------------------- |
| 8 | funcs() { |
| 9 | local func_file="${1:-$HOME/.func}" |
| 10 | |
| 11 | if [ ! -f "$func_file" ]; then |
| 12 | printf "Error: Function file '%s' not found.\n" "$func_file" >&2 |
| 13 | return 1 |
| 14 | fi |
| 15 | |
| 16 | printf "Available functions in %s:\n" "$func_file" |
| 17 | awk ' |
| 18 | /^# Function: / { |
| 19 | line = $0 |
| 20 | sub(/^# Function: /, "", line) |
| 21 | |
| 22 | name = line |
| 23 | sub(/ .*/, "", name) |
| 24 | |
| 25 | desc = "" |
| 26 | if (line ~ / \(.+\)$/) { |
| 27 | desc = line |
| 28 | sub(/^[^ ]+ \(/, "", desc) |
| 29 | sub(/\)$/, "", desc) |
| 30 | } |
| 31 | |
| 32 | if (desc != "") { |
| 33 | printf " %-22s %s\n", name, desc |
| 34 | } else { |
| 35 | printf " %s\n", name |
| 36 | } |
| 37 | } |
| 38 | ' "$func_file" |
| 39 | } |
| 40 | |
| 41 | # ----------------------------------------------------------------------------- |
| 42 | # Function: clip (Cross-platform clipboard) |
| 43 | # ----------------------------------------------------------------------------- |
| 44 | clip() { |
| 45 | local cmd |
| 46 | local args=() |
| 47 | |
| 48 | if command -v pbcopy >/dev/null 2>&1; then |
| 49 | cmd="pbcopy" # macOS |
| 50 | elif grep -qi "microsoft" /proc/version 2>/dev/null && command -v clip.exe >/dev/null 2>&1; then |
| 51 | cmd="clip.exe" # WSL |
| 52 | elif [ "$XDG_SESSION_TYPE" = "wayland" ] && command -v wl-copy >/dev/null 2>&1; then |
| 53 | cmd="wl-copy" # Linux Wayland |
| 54 | elif command -v xclip >/dev/null 2>&1; then |
| 55 | cmd="xclip" # Linux X11 (Fallback 1) |
| 56 | args=("-selection" "clipboard") |
| 57 | elif command -v xsel >/dev/null 2>&1; then |
| 58 | cmd="xsel" # Linux X11 (Fallback 2) |
| 59 | args=("--clipboard" "--input") |
| 60 | else |
| 61 | printf "Error: No supported clipboard utility found.\n" >&2 |
| 62 | return 1 |
| 63 | fi |
| 64 | |
| 65 | if [ $# -gt 0 ]; then |
| 66 | if [ -f "$1" ]; then |
| 67 | "$cmd" "${args[@]}" < "$1" |
| 68 | echo "Copied contents of '$1' to clipboard." |
| 69 | else |
| 70 | printf "Error: File '%s' not found.\n" "$1" >&2 |
| 71 | return 1 |
| 72 | fi |
| 73 | else |
| 74 | "$cmd" "${args[@]}" |
| 75 | fi |
| 76 | } |
| 77 | |
| 78 | # ----------------------------------------------------------------------------- |
| 79 | # Function: open_file (Cross-platform file/directory opener) |
| 80 | # ----------------------------------------------------------------------------- |
| 81 | open_file() { |
| 82 | local target="${1:-.}" |
| 83 | |
| 84 | if [[ "$OSTYPE" == "darwin"* ]]; then |
| 85 | command open "$target" |
| 86 | elif grep -qi "microsoft" /proc/version 2>/dev/null; then |
| 87 | if command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1; then |
| 88 | explorer.exe "$(wslpath -w "$target")" |
| 89 | else |
| 90 | printf "Error: 'wslpath' or 'explorer.exe' not found.\n" >&2 |
| 91 | return 1 |
| 92 | fi |
| 93 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then |
| 94 | if command -v xdg-open >/dev/null 2>&1; then |
| 95 | xdg-open "$target" |
| 96 | else |
| 97 | printf "Error: 'xdg-open' not found.\n" >&2 |
| 98 | return 1 |
| 99 | fi |
| 100 | fi |
| 101 | } |
| 102 | alias open='open_file' |
| 103 | |
| 104 | # ----------------------------------------------------------------------------- |
| 105 | # Function: clear_history (Supports shell, Claude, Codex, and OpenCode) |
| 106 | # ----------------------------------------------------------------------------- |
| 107 | clear_history() { |
| 108 | case "${1:-}" in |
| 109 | claude) |
| 110 | if [ -d "$HOME/.claude/projects" ]; then |
| 111 | # Use 'yes' to skip prompts and -f to ignore non-existent files |
| 112 | yes | rm -rf "$HOME/.claude/projects"/* |
| 113 | echo "Claude project history/cache cleared." |
| 114 | fi |
| 115 | ;; |
| 116 | |
| 117 | codex) |
| 118 | local codex_home="${CODEX_HOME:-$HOME/.codex}" |
| 119 | local codex_session_file |
| 120 | local codex_session_id |
| 121 | |
| 122 | mkdir -p "$codex_home" |
| 123 | : > "$codex_home/history.jsonl" |
| 124 | |
| 125 | if command -v codex >/dev/null 2>&1; then |
| 126 | find "$codex_home/sessions" "$codex_home/archived_sessions" -type f -name '*.jsonl' -print 2>/dev/null | while IFS= read -r codex_session_file; do |
| 127 | codex_session_id="$(sed -n 's/.*"session_id":"\([^"]*\)".*/\1/p; q' "$codex_session_file")" |
| 128 | if [ -n "$codex_session_id" ]; then |
| 129 | codex delete --force "$codex_session_id" >/dev/null 2>&1 || true |
| 130 | fi |
| 131 | done |
| 132 | fi |
| 133 | |
| 134 | rm -rf "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" |
| 135 | mkdir -p "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" |
| 136 | |
| 137 | echo "Codex history cleared." |
| 138 | ;; |
| 139 | |
| 140 | opencode) |
| 141 | local opencode_state_home="${XDG_STATE_HOME:-$HOME/.local/state}/opencode" |
| 142 | local opencode_data_home="${XDG_DATA_HOME:-$HOME/.local/share}/opencode" |
| 143 | |
| 144 | mkdir -p "$opencode_state_home" "$opencode_data_home" |
| 145 | : > "$opencode_state_home/prompt-history.jsonl" |
| 146 | rm -f \ |
| 147 | "$opencode_data_home/opencode.db" \ |
| 148 | "$opencode_data_home/opencode.db-shm" \ |
| 149 | "$opencode_data_home/opencode.db-wal" |
| 150 | rm -rf "$opencode_data_home/repos" "$opencode_data_home/log" |
| 151 | mkdir -p "$opencode_data_home/repos" "$opencode_data_home/log" |
| 152 | |
| 153 | echo "OpenCode history cleared." |
| 154 | ;; |
| 155 | |
| 156 | *) |
| 157 | # 1. Truncate the file |
| 158 | : > "$HISTFILE" |
| 159 | |
| 160 | # 2. Clear RAM by briefly setting history size to 0 |
| 161 | local old_histsize=$HISTSIZE |
| 162 | HISTSIZE=0 |
| 163 | HISTSIZE=$old_histsize |
| 164 | |
| 165 | echo "Shell history cleared." |
| 166 | ;; |
| 167 | esac |
| 168 | } |
| 169 | |
| 170 | # ----------------------------------------------------------------------------- |
| 171 | # Function: require_cli (Check external CLI availability) |
| 172 | # ----------------------------------------------------------------------------- |
| 173 | require_cli() { |
| 174 | local binary="$1" |
| 175 | local label="$2" |
| 176 | local resolved |
| 177 | |
| 178 | if command -v whence >/dev/null 2>&1; then |
| 179 | if whence -p "$binary" >/dev/null 2>&1; then |
| 180 | return 0 |
| 181 | fi |
| 182 | else |
| 183 | resolved="$(command -v "$binary" 2>/dev/null)" || resolved="" |
| 184 | if [ -n "$resolved" ] && [ -x "$resolved" ]; then |
| 185 | return 0 |
| 186 | fi |
| 187 | fi |
| 188 | |
| 189 | printf "Error: %s ('%s') is not installed or not in your PATH.\n" "$label" "$binary" >&2 |
| 190 | return 1 |
| 191 | } |
| 192 | |
| 193 | # ----------------------------------------------------------------------------- |
| 194 | # Function: claude (Includes --yolo and clear shortcuts) |
| 195 | # ----------------------------------------------------------------------------- |
| 196 | claude() { |
| 197 | # Shortcut for clearing cache |
| 198 | if [[ "$1" == "clear" ]]; then |
| 199 | clear_history claude |
| 200 | return 0 |
| 201 | fi |
| 202 | |
| 203 | # Check if binary exists |
| 204 | require_cli claude "Claude Code CLI" || return 1 |
| 205 | |
| 206 | # Handle --yolo mode |
| 207 | if [[ "$1" == "--yolo" ]]; then |
| 208 | shift |
| 209 | command claude --dangerously-skip-permissions "$@" |
| 210 | return $? |
| 211 | fi |
| 212 | |
| 213 | command claude "$@" |
| 214 | } |
| 215 | |
| 216 | # ----------------------------------------------------------------------------- |
| 217 | # Internal: _prompt_confirm_indexing (Confirm project indexing via the terminal) |
| 218 | # ----------------------------------------------------------------------------- |
| 219 | _prompt_confirm_indexing() { |
| 220 | local project_root="$1" |
| 221 | local reply |
| 222 | |
| 223 | if ! (: </dev/tty) 2>/dev/null; then |
| 224 | printf "Notice: No interactive terminal available; enhancing without project indexing.\n" >&2 |
| 225 | return 1 |
| 226 | fi |
| 227 | |
| 228 | printf "Allow Auggie to index and use project context from '%s'? (y/N) " "$project_root" >/dev/tty |
| 229 | if ! IFS= read -r reply </dev/tty; then |
| 230 | printf "\nNotice: Unable to read confirmation; enhancing without project indexing.\n" >&2 |
| 231 | return 1 |
| 232 | fi |
| 233 | |
| 234 | case "$reply" in |
| 235 | y|Y|yes|YES|Yes) |
| 236 | return 0 |
| 237 | ;; |
| 238 | *) |
| 239 | return 1 |
| 240 | ;; |
| 241 | esac |
| 242 | } |
| 243 | |
| 244 | # ----------------------------------------------------------------------------- |
| 245 | # Internal: _enhance_prompt (Run Auggie and write only the enhanced prompt) |
| 246 | # ----------------------------------------------------------------------------- |
| 247 | _enhance_prompt() { |
| 248 | local prompt="$1" |
| 249 | local output_file="$2" |
| 250 | local run_dir |
| 251 | local prompt_file |
| 252 | local workspace |
| 253 | local cache_dir |
| 254 | local auth_file |
| 255 | local log_file |
| 256 | local parsed_file |
| 257 | local project_root |
| 258 | local use_project_context |
| 259 | local script_pid |
| 260 | local waited |
| 261 | local timeout_seconds |
| 262 | local monitor_status |
| 263 | |
| 264 | if [ -z "$prompt" ]; then |
| 265 | printf "Error: Prompt was empty.\n" >&2 |
| 266 | return 1 |
| 267 | fi |
| 268 | |
| 269 | if [ ! -s "$HOME/.augment/session.json" ]; then |
| 270 | printf "Error: Auggie session file not found. Run 'auggie login' first.\n" >&2 |
| 271 | return 1 |
| 272 | fi |
| 273 | |
| 274 | run_dir="$(mktemp -d "${TMPDIR:-/tmp}/auggie-enhance.XXXXXX")" || return 1 |
| 275 | prompt_file="$run_dir/prompt.txt" |
| 276 | workspace="$run_dir/workspace" |
| 277 | cache_dir="$run_dir/cache" |
| 278 | auth_file="$HOME/.augment/session.json" |
| 279 | log_file="$run_dir/auggie.log" |
| 280 | parsed_file="$run_dir/enhanced.txt" |
| 281 | mkdir -p "$workspace" "$cache_dir" |
| 282 | printf "%s\n" "$prompt" > "$prompt_file" |
| 283 | |
| 284 | project_root="$(git rev-parse --show-toplevel 2>/dev/null)" || project_root="$PWD" |
| 285 | use_project_context=0 |
| 286 | timeout_seconds=90 |
| 287 | |
| 288 | if _prompt_confirm_indexing "$project_root"; then |
| 289 | use_project_context=1 |
| 290 | timeout_seconds=300 |
| 291 | workspace="$project_root" |
| 292 | cache_dir="$HOME/.augment" |
| 293 | printf "Indexing approved; enhancing with project context from '%s'.\n" "$project_root" >&2 |
| 294 | else |
| 295 | printf "Enhancing without project indexing.\n" >&2 |
| 296 | fi |
| 297 | |
| 298 | ( |
| 299 | if [ "$use_project_context" -eq 1 ]; then |
| 300 | AUGGIE_PROMPT_FILE="$prompt_file" \ |
| 301 | AUGGIE_WORKSPACE="$workspace" \ |
| 302 | AUGGIE_CACHE_DIR="$cache_dir" \ |
| 303 | AUGGIE_AUTH_FILE="$auth_file" \ |
| 304 | script -q -f -O "$log_file" -c 'auggie --print --enhance-prompt --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --allow-indexing --wait-for-indexing --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE"' </dev/null >/dev/null 2>&1 & |
| 305 | else |
| 306 | AUGGIE_PROMPT_FILE="$prompt_file" \ |
| 307 | AUGGIE_WORKSPACE="$workspace" \ |
| 308 | AUGGIE_CACHE_DIR="$cache_dir" \ |
| 309 | AUGGIE_AUTH_FILE="$auth_file" \ |
| 310 | script -q -f -O "$log_file" -c 'auggie --print --enhance-prompt --no-discover-workspaces --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE"' </dev/null >/dev/null 2>&1 & |
| 311 | fi |
| 312 | |
| 313 | script_pid=$! |
| 314 | waited=0 |
| 315 | |
| 316 | while kill -0 "$script_pid" >/dev/null 2>&1; do |
| 317 | if grep -aq "🤖" "$log_file" 2>/dev/null || grep -aq "Tool call:" "$log_file" 2>/dev/null; then |
| 318 | kill "$script_pid" >/dev/null 2>&1 || true |
| 319 | wait "$script_pid" >/dev/null 2>&1 || true |
| 320 | exit 0 |
| 321 | fi |
| 322 | |
| 323 | if [ "$waited" -ge "$timeout_seconds" ]; then |
| 324 | kill "$script_pid" >/dev/null 2>&1 || true |
| 325 | wait "$script_pid" >/dev/null 2>&1 || true |
| 326 | exit 124 |
| 327 | fi |
| 328 | |
| 329 | sleep 1 |
| 330 | waited=$((waited + 1)) |
| 331 | done |
| 332 | |
| 333 | wait "$script_pid" >/dev/null 2>&1 || true |
| 334 | exit 0 |
| 335 | ) |
| 336 | monitor_status=$? |
| 337 | |
| 338 | if [ "$monitor_status" -eq 124 ]; then |
| 339 | printf "Error: Timed out waiting for Auggie to enhance the prompt.\n" >&2 |
| 340 | rm -rf "$run_dir" |
| 341 | return 124 |
| 342 | fi |
| 343 | |
| 344 | perl -ne ' |
| 345 | # Auggie redraws streamed terminal lines with carriage returns. Keep only |
| 346 | # the final rendered segment so stale text cannot corrupt the prompt. |
| 347 | s/\r$//; |
| 348 | s/.*\r//; |
| 349 | 1 while s/[^\x08]\x08//g; |
| 350 | s/\x08//g; |
| 351 | s/\e\][^\a]*(?:\a|\e\\)//g; |
| 352 | s/\e\[[0-?]*[ -\/]*[@-~]//g; |
| 353 | next if /Script started on/ || /Script done on/; |
| 354 | if (/^(?:✨\s*)?Enhanced prompt:\s*(.*)$/) { |
| 355 | $capturing = 1; |
| 356 | $output .= "$1\n" if length $1; |
| 357 | next; |
| 358 | } |
| 359 | next unless $capturing; |
| 360 | exit if /^🤖/ || /Tool call:/ || /Session terminated/; |
| 361 | $output .= $_; |
| 362 | END { |
| 363 | $output =~ s/^\s*\n//; |
| 364 | $output =~ s/\s+\z//; |
| 365 | print "$output\n" if length $output; |
| 366 | } |
| 367 | ' "$log_file" > "$parsed_file" |
| 368 | |
| 369 | if [ ! -s "$parsed_file" ]; then |
| 370 | printf "Error: Auggie did not return an enhanced prompt.\n" >&2 |
| 371 | rm -rf "$run_dir" |
| 372 | return 1 |
| 373 | fi |
| 374 | |
| 375 | if ! command cp "$parsed_file" "$output_file"; then |
| 376 | printf "Error: Unable to save the enhanced prompt.\n" >&2 |
| 377 | rm -rf "$run_dir" |
| 378 | return 1 |
| 379 | fi |
| 380 | rm -rf "$run_dir" |
| 381 | } |
| 382 | |
| 383 | # ----------------------------------------------------------------------------- |
| 384 | # Function: prompt (Enhance a prompt with Auggie and optional project context) |
| 385 | # ----------------------------------------------------------------------------- |
| 386 | prompt() { |
| 387 | local prompt |
| 388 | local line |
| 389 | local output_file |
| 390 | |
| 391 | require_cli auggie "Auggie CLI" || return 1 |
| 392 | require_cli script "script utility" || return 1 |
| 393 | |
| 394 | if [ $# -gt 0 ]; then |
| 395 | prompt="$*" |
| 396 | elif [ ! -t 0 ]; then |
| 397 | prompt="$(cat)" |
| 398 | else |
| 399 | echo "Reading prompt... Type 'EOF' on a new line and press Enter when finished." |
| 400 | prompt="" |
| 401 | |
| 402 | while IFS= read -r line; do |
| 403 | [ "$line" = "EOF" ] && break |
| 404 | if [ -z "$prompt" ]; then |
| 405 | prompt="$line" |
| 406 | else |
| 407 | prompt="${prompt}"$'\n'"${line}" |
| 408 | fi |
| 409 | done |
| 410 | fi |
| 411 | |
| 412 | if [ -z "$prompt" ]; then |
| 413 | printf "Error: Prompt was empty.\n" >&2 |
| 414 | return 1 |
| 415 | fi |
| 416 | |
| 417 | output_file="$(mktemp "${TMPDIR:-/tmp}/auggie-enhanced-prompt.XXXXXX")" || return 1 |
| 418 | if ! _enhance_prompt "$prompt" "$output_file"; then |
| 419 | rm -f "$output_file" |
| 420 | return 1 |
| 421 | fi |
| 422 | |
| 423 | if [ -t 1 ]; then |
| 424 | clear |
| 425 | fi |
| 426 | |
| 427 | printf "Enhanced prompt:\n" |
| 428 | command cat "$output_file" |
| 429 | rm -f "$output_file" |
| 430 | |
| 431 | command auggie account status |
| 432 | } |
| 433 | |
| 434 | # ----------------------------------------------------------------------------- |
| 435 | # Function: plan_build (Cached canonical plan-build launcher) |
| 436 | # ----------------------------------------------------------------------------- |
| 437 | plan_build() { |
| 438 | emulate -L zsh |
| 439 | setopt localtraps |
| 440 | local base_url="${PLAN_BUILD_BASE_URL:-https://opengist.resetrix.work/weehong/plan-build/raw/HEAD}" |
| 441 | local cache_dir="${PLAN_BUILD_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/plan-build}" |
| 442 | local curl_command="${PLAN_BUILD_CURL_COMMAND:-curl}" |
| 443 | local skill_file="$HOME/.claude/skills/plan-build/SKILL.md" |
| 444 | local architect_file="$HOME/.claude/skills/plan-build-architect/SKILL.md" |
| 445 | local stage_dir="" release_dir="" current_tmp="" |
| 446 | local skill_tmp="" architect_tmp="" skill_backup="" architect_backup="" |
| 447 | local activation_lock="$cache_dir/.activate.lock" |
| 448 | local activation_lock_fd="" |
| 449 | local selected_release="" active_pid="" refresh_rc=1 call_rc=0 interrupted_rc=0 |
| 450 | |
| 451 | if ! zmodload zsh/system 2>/dev/null; then |
| 452 | printf "Error: Unable to load the Zsh system module required by plan-build.\n" >&2 |
| 453 | return 1 |
| 454 | fi |
| 455 | |
| 456 | require_cli zsh "Zsh" || return 1 |
| 457 | |
| 458 | if ! command mkdir -p "$cache_dir" "${skill_file:h}" "${architect_file:h}"; then |
| 459 | printf "Error: Unable to create plan-build cache or skill directories.\n" >&2 |
| 460 | return 1 |
| 461 | fi |
| 462 | |
| 463 | _plan_build_cleanup() { |
| 464 | if [[ -n "${active_pid:-}" ]] && command kill -0 "$active_pid" 2>/dev/null; then |
| 465 | command kill "$active_pid" 2>/dev/null || true |
| 466 | wait "$active_pid" 2>/dev/null || true |
| 467 | fi |
| 468 | active_pid="" |
| 469 | [[ -n "$stage_dir" && -d "$stage_dir" ]] && command rm -rf -- "$stage_dir" |
| 470 | command rm -f -- "$current_tmp" "$skill_tmp" "$architect_tmp" |
| 471 | command rm -f -- "$skill_backup" "$architect_backup" |
| 472 | if [[ -n "${activation_lock_fd:-}" ]]; then |
| 473 | zsystem flock -u "$activation_lock_fd" 2>/dev/null || true |
| 474 | activation_lock_fd="" |
| 475 | fi |
| 476 | } |
| 477 | _plan_build_cancel() { |
| 478 | interrupted_rc="$1" |
| 479 | [[ -n "${active_pid:-}" ]] && command kill "$active_pid" 2>/dev/null || true |
| 480 | } |
| 481 | trap '_plan_build_cleanup' EXIT |
| 482 | trap '_plan_build_cancel 129' HUP |
| 483 | trap '_plan_build_cancel 130' INT |
| 484 | trap '_plan_build_cancel 143' TERM |
| 485 | |
| 486 | _plan_build_validate_release() { |
| 487 | local candidate="$1" |
| 488 | [[ -d "$candidate" && |
| 489 | -s "$candidate/plan_build.zsh" && |
| 490 | -s "$candidate/SKILL.md" && |
| 491 | -s "$candidate/ARCHITECT.md" ]] && |
| 492 | command zsh -n "$candidate/plan_build.zsh" |
| 493 | } |
| 494 | |
| 495 | _plan_build_replace_path() { |
| 496 | local replacement="$1" destination="$2" |
| 497 | if command mv -fT "$replacement" "$destination" 2>/dev/null; then |
| 498 | return 0 |
| 499 | fi |
| 500 | command mv -fh "$replacement" "$destination" |
| 501 | } |
| 502 | |
| 503 | _plan_build_select_current() { |
| 504 | local resolved |
| 505 | [[ -L "$cache_dir/current" ]] || return 1 |
| 506 | resolved="$cache_dir/current" |
| 507 | resolved="${resolved:A}" |
| 508 | _plan_build_validate_release "$resolved" || return 1 |
| 509 | selected_release="$resolved" |
| 510 | } |
| 511 | |
| 512 | _plan_build_acquire_activation_lock() { |
| 513 | (( interrupted_rc )) && return 1 |
| 514 | command touch "$activation_lock" || return 1 |
| 515 | zsystem flock -t 10 -f activation_lock_fd "$activation_lock" |
| 516 | } |
| 517 | |
| 518 | _plan_build_release_activation_lock() { |
| 519 | [[ -n "$activation_lock_fd" ]] || return 0 |
| 520 | zsystem flock -u "$activation_lock_fd" || return 1 |
| 521 | activation_lock_fd="" |
| 522 | } |
| 523 | |
| 524 | _plan_build_download() { |
| 525 | local remote="$1" destination="$2" download_rc |
| 526 | command "$curl_command" -fsSL "$remote" -o "$destination" & |
| 527 | active_pid=$! |
| 528 | wait "$active_pid" |
| 529 | download_rc=$? |
| 530 | active_pid="" |
| 531 | (( interrupted_rc )) && return "$interrupted_rc" |
| 532 | return "$download_rc" |
| 533 | } |
| 534 | |
| 535 | _plan_build_backup_path() { |
| 536 | local source="$1" backup="$2" |
| 537 | [[ -e "$source" || -L "$source" ]] || return 0 |
| 538 | command rm -f -- "$backup" && command cp -RP "$source" "$backup" |
| 539 | } |
| 540 | |
| 541 | _plan_build_restore_path() { |
| 542 | local destination="$1" backup="$2" had_old="$3" |
| 543 | if (( had_old )); then |
| 544 | _plan_build_replace_path "$backup" "$destination" || return 1 |
| 545 | else |
| 546 | command rm -f -- "$destination" |
| 547 | fi |
| 548 | } |
| 549 | |
| 550 | _plan_build_install_skill_links() { |
| 551 | local activation_ok=1 |
| 552 | local skill_had_old=0 architect_had_old=0 |
| 553 | |
| 554 | skill_tmp="$(command mktemp "${skill_file}.link.XXXXXX")" && |
| 555 | command rm -f "$skill_tmp" && |
| 556 | command ln -s "$cache_dir/current/SKILL.md" "$skill_tmp" || skill_tmp="" |
| 557 | architect_tmp="$(command mktemp "${architect_file}.link.XXXXXX")" && |
| 558 | command rm -f "$architect_tmp" && |
| 559 | command ln -s "$cache_dir/current/ARCHITECT.md" "$architect_tmp" || architect_tmp="" |
| 560 | [[ -n "$skill_tmp" && -L "$skill_tmp" && |
| 561 | -n "$architect_tmp" && -L "$architect_tmp" ]] || return 1 |
| 562 | |
| 563 | skill_backup="${skill_file}.backup.$$.$RANDOM" |
| 564 | architect_backup="${architect_file}.backup.$$.$RANDOM" |
| 565 | [[ -e "$skill_file" || -L "$skill_file" ]] && skill_had_old=1 |
| 566 | [[ -e "$architect_file" || -L "$architect_file" ]] && architect_had_old=1 |
| 567 | _plan_build_backup_path "$skill_file" "$skill_backup" || activation_ok=0 |
| 568 | (( activation_ok )) && _plan_build_backup_path "$architect_file" "$architect_backup" || activation_ok=0 |
| 569 | (( activation_ok && ! interrupted_rc )) && |
| 570 | _plan_build_replace_path "$skill_tmp" "$skill_file" || activation_ok=0 |
| 571 | (( activation_ok )) && skill_tmp="" |
| 572 | (( activation_ok && ! interrupted_rc )) && |
| 573 | _plan_build_replace_path "$architect_tmp" "$architect_file" || activation_ok=0 |
| 574 | (( activation_ok )) && architect_tmp="" |
| 575 | |
| 576 | if (( activation_ok )); then |
| 577 | command rm -f -- "$skill_backup" "$architect_backup" |
| 578 | skill_backup="" architect_backup="" |
| 579 | return 0 |
| 580 | fi |
| 581 | |
| 582 | if ! _plan_build_restore_path "$skill_file" "$skill_backup" "$skill_had_old"; then |
| 583 | printf "Error: Unable to restore plan-build skill; backup preserved at %s.\n" "$skill_backup" >&2 |
| 584 | skill_backup="" |
| 585 | fi |
| 586 | if ! _plan_build_restore_path "$architect_file" "$architect_backup" "$architect_had_old"; then |
| 587 | printf "Error: Unable to restore architect skill; backup preserved at %s.\n" "$architect_backup" >&2 |
| 588 | architect_backup="" |
| 589 | fi |
| 590 | return 1 |
| 591 | } |
| 592 | |
| 593 | if (( $+commands[$curl_command] )) || [[ "$curl_command" == */* && -x "$curl_command" ]]; then |
| 594 | stage_dir="$(command mktemp -d "$cache_dir/.stage.XXXXXX")" || stage_dir="" |
| 595 | if [[ -n "$stage_dir" ]]; then |
| 596 | _plan_build_download "$base_url/plan_build.zsh" "$stage_dir/plan_build.zsh" && |
| 597 | _plan_build_download "$base_url/SKILL.md" "$stage_dir/SKILL.md" && |
| 598 | _plan_build_download "$base_url/ARCHITECT.md" "$stage_dir/ARCHITECT.md" |
| 599 | refresh_rc=$? |
| 600 | if (( interrupted_rc )); then |
| 601 | _plan_build_cleanup |
| 602 | trap - EXIT HUP INT TERM |
| 603 | return "$interrupted_rc" |
| 604 | fi |
| 605 | else |
| 606 | refresh_rc=1 |
| 607 | fi |
| 608 | if (( refresh_rc == 0 )) && |
| 609 | _plan_build_validate_release "$stage_dir"; then |
| 610 | release_dir="$(command mktemp -d "$cache_dir/release.XXXXXX")" || release_dir="" |
| 611 | if [[ -n "$release_dir" ]] && |
| 612 | command rmdir "$release_dir" && |
| 613 | command mv "$stage_dir" "$release_dir" && |
| 614 | _plan_build_acquire_activation_lock; then |
| 615 | stage_dir="" |
| 616 | release_dir="${release_dir:A}" |
| 617 | current_tmp="$(command mktemp "$cache_dir/.current.XXXXXX")" && |
| 618 | command rm -f "$current_tmp" && |
| 619 | command ln -s "$release_dir" "$current_tmp" || current_tmp="" |
| 620 | |
| 621 | if [[ -n "$current_tmp" && -L "$current_tmp" ]] && |
| 622 | _plan_build_install_skill_links && |
| 623 | _plan_build_replace_path "$current_tmp" "$cache_dir/current"; then |
| 624 | current_tmp="" |
| 625 | selected_release="$release_dir" |
| 626 | fi |
| 627 | _plan_build_release_activation_lock || true |
| 628 | fi |
| 629 | fi |
| 630 | fi |
| 631 | |
| 632 | if (( interrupted_rc )); then |
| 633 | _plan_build_cleanup |
| 634 | trap - EXIT HUP INT TERM |
| 635 | return "$interrupted_rc" |
| 636 | fi |
| 637 | |
| 638 | if [[ -z "$selected_release" ]]; then |
| 639 | if ! _plan_build_select_current; then |
| 640 | if (( ! $+commands[$curl_command] )) && [[ ! ( "$curl_command" == */* && -x "$curl_command" ) ]]; then |
| 641 | printf "Error: curl is required because no valid cached plan-build bundle is available.\n" >&2 |
| 642 | else |
| 643 | printf "Error: Unable to refresh plan-build and no valid cached bundle is available.\n" >&2 |
| 644 | fi |
| 645 | return 1 |
| 646 | fi |
| 647 | if (( ! $+commands[$curl_command] )) && [[ ! ( "$curl_command" == */* && -x "$curl_command" ) ]]; then |
| 648 | printf "Warning: curl is unavailable; using the validated cached plan-build bundle.\n" >&2 |
| 649 | else |
| 650 | printf "Warning: Unable to refresh plan-build; using the validated cached bundle.\n" >&2 |
| 651 | fi |
| 652 | fi |
| 653 | |
| 654 | if [[ "$(command readlink "$skill_file" 2>/dev/null)" != "$cache_dir/current/SKILL.md" || |
| 655 | "$(command readlink "$architect_file" 2>/dev/null)" != "$cache_dir/current/ARCHITECT.md" ]]; then |
| 656 | if ! _plan_build_acquire_activation_lock || ! _plan_build_install_skill_links; then |
| 657 | _plan_build_release_activation_lock 2>/dev/null || true |
| 658 | if (( interrupted_rc )); then |
| 659 | _plan_build_cleanup |
| 660 | trap - EXIT HUP INT TERM |
| 661 | return "$interrupted_rc" |
| 662 | fi |
| 663 | printf "Error: Unable to install the plan-build skill links.\n" >&2 |
| 664 | return 1 |
| 665 | fi |
| 666 | _plan_build_release_activation_lock || true |
| 667 | fi |
| 668 | |
| 669 | if (( interrupted_rc )); then |
| 670 | _plan_build_cleanup |
| 671 | trap - EXIT HUP INT TERM |
| 672 | return "$interrupted_rc" |
| 673 | fi |
| 674 | |
| 675 | PLAN_BUILD_SKILL_PATH="$selected_release/SKILL.md" \ |
| 676 | PLAN_BUILD_ARCHITECT_SKILL_PATH="$selected_release/ARCHITECT.md" \ |
| 677 | command zsh "$selected_release/plan_build.zsh" "$@" & |
| 678 | active_pid=$! |
| 679 | wait "$active_pid" |
| 680 | call_rc=$? |
| 681 | active_pid="" |
| 682 | (( interrupted_rc )) && call_rc="$interrupted_rc" |
| 683 | _plan_build_cleanup |
| 684 | trap - EXIT HUP INT TERM |
| 685 | return "$call_rc" |
| 686 | } |
| 687 | |
| 688 | # ----------------------------------------------------------------------------- |
| 689 | # Function: aicommit (Automated Conventional Commit Engine Wrapper) |
| 690 | # ----------------------------------------------------------------------------- |
| 691 | aicommit() { |
| 692 | local diff engine prompt |
| 693 | |
| 694 | # 1. Get diff safely: exclude lock files/minified files and cap at ~100k characters to prevent CLI crashes |
| 695 | diff=$(git diff --cached -- . ":(exclude)*.lock" ":(exclude)*-lock.json" ":(exclude)*.min.js" | head -c 100000) |
| 696 | |
| 697 | if [ -z "$diff" ]; then |
| 698 | printf "❌ Error: Nothing staged to commit.\n" >&2 |
| 699 | return 1 |
| 700 | fi |
| 701 | |
| 702 | if [ ! -f "$HOME/.config/ai-commit-prompt.txt" ]; then |
| 703 | printf "❌ Error: Configuration file not found at ~/.config/ai-commit-prompt.txt\n" >&2 |
| 704 | printf "Please create it and paste your system prompt inside.\n" >&2 |
| 705 | return 1 |
| 706 | fi |
| 707 | |
| 708 | engine=${1:-claude} |
| 709 | prompt=$(cat "$HOME/.config/ai-commit-prompt.txt") |
| 710 | |
| 711 | case "$engine" in |
| 712 | claude) |
| 713 | echo "🤖 Claude is analyzing staged changes..." |
| 714 | cat "$HOME/.config/ai-commit-prompt.txt" <(echo -e "\n\nHere is the git diff:\n$diff") \ |
| 715 | | command claude --allowedTools 'Bash(git commit *)' --permission-mode dontAsk -p \ |
| 716 | "Read the provided instructions and diff, then generate and execute the commit." |
| 717 | ;; |
| 718 | |
| 719 | codex) |
| 720 | echo "🤖 Codex is analyzing staged changes..." |
| 721 | cat "$HOME/.config/ai-commit-prompt.txt" <(echo -e "\n\nHere is the git diff:\n$diff") \ |
| 722 | | command codex --ask-for-approval never exec --sandbox workspace-write --ephemeral \ |
| 723 | "Read the provided instructions and diff, then generate and execute the commit." |
| 724 | ;; |
| 725 | |
| 726 | copilot) |
| 727 | echo "🤖 Copilot is analyzing staged changes..." |
| 728 | copilot -p "$prompt |
| 729 | |
| 730 | Here is the git diff: |
| 731 | $diff" -s --no-ask-user --allow-tool='shell(git:*)' |
| 732 | ;; |
| 733 | |
| 734 | opencode) |
| 735 | echo "🤖 OpenCode is analyzing staged changes..." |
| 736 | cat "$HOME/.config/ai-commit-prompt.txt" <(echo -e "\n\nHere is the git diff:\n$diff") | opencode run --agent "AI commit" |
| 737 | ;; |
| 738 | |
| 739 | *) |
| 740 | printf "❌ Error: Invalid engine '%s'\n" "$engine" >&2 |
| 741 | printf "Usage: aicommit [claude | codex | copilot | opencode]\n" >&2 |
| 742 | return 1 |
| 743 | ;; |
| 744 | esac |
| 745 | } |
| 746 | |
| 747 | # ----------------------------------------------------------------------------- |
| 748 | # Function: create (Efficiently create file + parent dirs) |
| 749 | # ----------------------------------------------------------------------------- |
| 750 | create() { |
| 751 | if ! command -v install >/dev/null 2>&1; then |
| 752 | printf "Error: 'install' coreutil is not available.\n" >&2 |
| 753 | return 1 |
| 754 | fi |
| 755 | |
| 756 | if [ $# -eq 0 ]; then |
| 757 | printf "Usage: create <file> [file ...]\n" >&2 |
| 758 | return 2 |
| 759 | fi |
| 760 | |
| 761 | for p in "$@"; do |
| 762 | # Expand tilde manually if shell doesn't |
| 763 | [[ "$p" == "~/"* ]] && p="${HOME}/${p#\~/}" |
| 764 | |
| 765 | if [ -z "$p" ] || [ "$p" = "/" ]; then |
| 766 | printf "create: refusing to operate on '%s'\n" "$p" >&2 |
| 767 | continue |
| 768 | fi |
| 769 | |
| 770 | if [ -e "$p" ]; then |
| 771 | printf "create: '%s' already exists. Skipping.\n" "$p" >&2 |
| 772 | continue |
| 773 | fi |
| 774 | |
| 775 | if install -D /dev/null "$p"; then |
| 776 | echo "Created '$p'" |
| 777 | fi |
| 778 | done |
| 779 | } |
| 780 | |
| 781 | # ----------------------------------------------------------------------------- |
| 782 | # Function: kill_port (Kill or check the process using a port) |
| 783 | # ----------------------------------------------------------------------------- |
| 784 | kill_port() { |
| 785 | local action="kill" |
| 786 | local signal="TERM" |
| 787 | local port |
| 788 | local pids |
| 789 | local pid |
| 790 | |
| 791 | case "${1:-}" in |
| 792 | -c|--check) |
| 793 | action="check" |
| 794 | shift |
| 795 | ;; |
| 796 | -k|--kill) |
| 797 | action="kill" |
| 798 | shift |
| 799 | ;; |
| 800 | -f|--force) |
| 801 | action="kill" |
| 802 | signal="KILL" |
| 803 | shift |
| 804 | ;; |
| 805 | -h|--help|"") |
| 806 | printf "Usage: kill_port [--check|-c|--kill|-k|--force|-f] <port_number>\n" >&2 |
| 807 | return 1 |
| 808 | ;; |
| 809 | esac |
| 810 | |
| 811 | port="${1:-}" |
| 812 | if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then |
| 813 | printf "Error: port must be a number between 1 and 65535.\n" >&2 |
| 814 | printf "Usage: kill_port [--check|-c|--kill|-k|--force|-f] <port_number>\n" >&2 |
| 815 | return 1 |
| 816 | fi |
| 817 | |
| 818 | if [[ "$OSTYPE" == "darwin"* ]]; then |
| 819 | pids="$( |
| 820 | { |
| 821 | lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null |
| 822 | lsof -nP -tiUDP:"$port" 2>/dev/null |
| 823 | } | sort -u |
| 824 | )" |
| 825 | |
| 826 | if [ -z "$pids" ]; then |
| 827 | printf "Port %s is FREE\n" "$port" |
| 828 | return 0 |
| 829 | fi |
| 830 | |
| 831 | printf "Port %s is in use:\n" "$port" |
| 832 | lsof -nP -iTCP:"$port" -sTCP:LISTEN -iUDP:"$port" 2>/dev/null |
| 833 | else |
| 834 | local port_hex |
| 835 | local inodes |
| 836 | |
| 837 | port_hex="$(printf "%04X" "$port")" |
| 838 | inodes="$( |
| 839 | awk -v port="$port_hex" ' |
| 840 | NR > 1 { |
| 841 | split($2, local_addr, ":") |
| 842 | if (toupper(local_addr[length(local_addr)]) == port && ($4 == "0A" || FILENAME ~ /udp/)) { |
| 843 | print $10 |
| 844 | } |
| 845 | } |
| 846 | ' /proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6 2>/dev/null | sort -u |
| 847 | )" |
| 848 | |
| 849 | if [ -z "$inodes" ]; then |
| 850 | printf "Port %s is FREE\n" "$port" |
| 851 | return 0 |
| 852 | fi |
| 853 | |
| 854 | pids="" |
| 855 | pids="$( |
| 856 | find /proc/[0-9]*/fd -maxdepth 1 -type l -printf "%p %l\n" 2>/dev/null | |
| 857 | awk -v inodes="$inodes" ' |
| 858 | BEGIN { |
| 859 | split(inodes, inode_list, " ") |
| 860 | for (i in inode_list) { |
| 861 | wanted["socket:[" inode_list[i] "]"] = 1 |
| 862 | } |
| 863 | } |
| 864 | $2 in wanted { |
| 865 | split($1, path, "/") |
| 866 | print path[3] |
| 867 | } |
| 868 | ' | sort -u |
| 869 | )" |
| 870 | |
| 871 | if [ -z "$pids" ]; then |
| 872 | printf "Port %s is in use, but no readable process owner was found.\n" "$port" >&2 |
| 873 | return 1 |
| 874 | fi |
| 875 | |
| 876 | printf "Port %s is in use:\n" "$port" |
| 877 | for pid in $pids; do |
| 878 | if [ -r "/proc/$pid/cmdline" ]; then |
| 879 | printf " PID %-8s %s\n" "$pid" "$(tr '\0' ' ' < "/proc/$pid/cmdline")" |
| 880 | else |
| 881 | printf " PID %s\n" "$pid" |
| 882 | fi |
| 883 | done |
| 884 | fi |
| 885 | |
| 886 | if [ "$action" = "kill" ]; then |
| 887 | for pid in $pids; do |
| 888 | if kill "-$signal" "$pid" 2>/dev/null; then |
| 889 | printf "Killed PID %s with SIG%s.\n" "$pid" "$signal" |
| 890 | else |
| 891 | printf "Error: failed to kill PID %s. Try with sudo or --force.\n" "$pid" >&2 |
| 892 | return 1 |
| 893 | fi |
| 894 | done |
| 895 | fi |
| 896 | } |
| 897 | |
| 898 | # ----------------------------------------------------------------------------- |
| 899 | # Function: append_path (Adds to session PATH if exists) |
| 900 | # ----------------------------------------------------------------------------- |
| 901 | append_path() { |
| 902 | local dir="$1" |
| 903 | if [ -d "$dir" ]; then |
| 904 | case ":$PATH:" in |
| 905 | *":$dir:"*) ;; |
| 906 | *) PATH="$PATH:$dir" ;; |
| 907 | esac |
| 908 | elif [ "${SUPPRESS_WARNINGS:-0}" -ne 1 ]; then |
| 909 | echo "Warning: $dir does not exist." >&2 |
| 910 | fi |
| 911 | } |
| 912 | |
| 913 | # ----------------------------------------------------------------------------- |
| 914 | # Function: add_path_to_config (Fixed awk/sed injection) |
| 915 | # ----------------------------------------------------------------------------- |
| 916 | add_path_to_config() { |
| 917 | local new_path="$1" |
| 918 | local config_file="$HOME/.bashrc" |
| 919 | [[ "$SHELL" == *"zsh"* ]] && config_file="$HOME/.zshrc" |
| 920 | |
| 921 | if [ -z "$new_path" ]; then |
| 922 | printf "Usage: add_path_to_config <directory>\n" >&2 |
| 923 | return 1 |
| 924 | fi |
| 925 | |
| 926 | new_path="${new_path/#\~/$HOME}" |
| 927 | |
| 928 | if [ ! -d "$new_path" ]; then |
| 929 | printf "Warning: '%s' does not exist. Add anyway? (y/N) " "$new_path" |
| 930 | read -r REPLY |
| 931 | [[ ! $REPLY =~ ^[Yy]$ ]] && return 1 |
| 932 | fi |
| 933 | |
| 934 | # Check if already exists in file |
| 935 | if grep -Fq "append_path \"$new_path\"" "$config_file"; then |
| 936 | printf "Notice: Already in %s.\n" "$config_file" |
| 937 | return 0 |
| 938 | fi |
| 939 | |
| 940 | # Robust Injection: Use sed to insert before export PATH or just append |
| 941 | if grep -q "^export PATH" "$config_file"; then |
| 942 | sed -i.bak "/^export PATH/i append_path \"$new_path\"" "$config_file" |
| 943 | else |
| 944 | echo "append_path \"$new_path\"" >> "$config_file" |
| 945 | echo "export PATH" >> "$config_file" |
| 946 | fi |
| 947 | |
| 948 | printf "✅ Success: Added to %s\n" "$config_file" |
| 949 | append_path "$new_path" && export PATH |
| 950 | } |
| 951 |
| 1 | # ============================================================================= |
| 2 | # ENVIRONMENT & PATH CONFIGURATION |
| 3 | # ============================================================================= |
| 4 | |
| 5 | # 1. Source functions first |
| 6 | if [[ -f "$HOME/.func" && -r "$HOME/.func" ]]; then |
| 7 | source "$HOME/.func" |
| 8 | fi |
| 9 | |
| 10 | # 2. Define Root Variables |
| 11 | export DOTNET_ROOT="$HOME/.dotnet" |
| 12 | |
| 13 | # 3. PATH INITIALIZATION |
| 14 | # ----------------------------------------------------------------------------- |
| 15 | append_path "$HOME/.local/bin" |
| 16 | append_path "$DOTNET_ROOT" |
| 17 | append_path "$DOTNET_ROOT/tools" |
| 18 | append_path "$HOME/.opencode/bin" |
| 19 | append_path "$HOME/Flutter/bin" |
| 20 | |
| 21 | # macOS specific paths |
| 22 | if [[ "$OSTYPE" == "darwin"* ]]; then |
| 23 | append_path "/Applications/Espanso.app/Contents/MacOS" |
| 24 | fi |
| 25 | |
| 26 | # Finalize PATH |
| 27 | export PATH |
| 1 | # ============================================================================= |
| 2 | # FILE: ~/.sourcerc |
| 3 | # Description: Initializes third-party package managers and external tools. |
| 4 | # ============================================================================= |
| 5 | |
| 6 | # ============================================================================= |
| 7 | # GENERAL ENV |
| 8 | # ============================================================================= |
| 9 | if [[ -f "$HOME/.local/bin/env" && -r "$HOME/.local/bin/env" ]]; then |
| 10 | source "$HOME/.local/bin/env" |
| 11 | fi |
| 12 | |
| 13 | # ============================================================================= |
| 14 | # CLAUDE CODE / AI GATEWAY |
| 15 | # ============================================================================= |
| 16 | |
| 17 | # ============================================================================= |
| 18 | # SDKMAN (Java/Kotlin/Scala Version Manager) |
| 19 | # ============================================================================= |
| 20 | export SDKMAN_DIR="$HOME/.sdkman" |
| 21 | if [[ -f "$SDKMAN_DIR/bin/sdkman-init.sh" && -r "$SDKMAN_DIR/bin/sdkman-init.sh" ]]; then |
| 22 | source "$SDKMAN_DIR/bin/sdkman-init.sh" |
| 23 | fi |
| 24 | |
| 25 | # ============================================================================= |
| 26 | # NVM (Node Version Manager) |
| 27 | # ============================================================================= |
| 28 | export NVM_DIR="$HOME/.nvm" |
| 29 | if [[ -f "$NVM_DIR/nvm.sh" && -r "$NVM_DIR/nvm.sh" ]]; then |
| 30 | source "$NVM_DIR/nvm.sh" |
| 31 | fi |
| 32 | if [[ -f "$NVM_DIR/bash_completion" && -r "$NVM_DIR/bash_completion" ]]; then |
| 33 | source "$NVM_DIR/bash_completion" |
| 34 | fi |
| 35 | |
| 36 | # ============================================================================= |
| 37 | # GOOGLE CLOUD SDK |
| 38 | # ============================================================================= |
| 39 | if [[ -f "/opt/google-cloud-sdk/path.zsh.inc" ]]; then |
| 40 | source "/opt/google-cloud-sdk/path.zsh.inc" |
| 41 | fi |
| 42 | |
| 43 | if [[ -f "/opt/google-cloud-sdk/completion.zsh.inc" ]]; then |
| 44 | source "/opt/google-cloud-sdk/completion.zsh.inc" |
| 45 | fi |
| 46 |
| 1 | "$schema" = 'https://starship.rs/config-schema.json' |
| 2 | |
| 3 | format = """ |
| 4 | [](red)\ |
| 5 | $os\ |
| 6 | $username\ |
| 7 | [](bg:peach fg:red)\ |
| 8 | $directory\ |
| 9 | [](bg:yellow fg:peach)\ |
| 10 | $git_branch\ |
| 11 | $git_status\ |
| 12 | [](fg:yellow bg:green)\ |
| 13 | $c\ |
| 14 | $rust\ |
| 15 | $golang\ |
| 16 | $nodejs\ |
| 17 | $bun\ |
| 18 | $php\ |
| 19 | $java\ |
| 20 | $kotlin\ |
| 21 | $haskell\ |
| 22 | $python\ |
| 23 | [](fg:green bg:sapphire)\ |
| 24 | $conda\ |
| 25 | [](fg:sapphire bg:lavender)\ |
| 26 | $time\ |
| 27 | [ ](fg:lavender)\ |
| 28 | $cmd_duration\ |
| 29 | $line_break\ |
| 30 | $character""" |
| 31 | |
| 32 | palette = 'catppuccin_mocha' |
| 33 | |
| 34 | [os] |
| 35 | disabled = false |
| 36 | style = "bg:red fg:crust" |
| 37 | |
| 38 | [os.symbols] |
| 39 | Windows = "" |
| 40 | Ubuntu = "" |
| 41 | SUSE = "" |
| 42 | Raspbian = "" |
| 43 | Mint = "" |
| 44 | Macos = "" |
| 45 | Manjaro = "" |
| 46 | Linux = "" |
| 47 | Gentoo = "" |
| 48 | Fedora = "" |
| 49 | Alpine = "" |
| 50 | Amazon = "" |
| 51 | Android = "" |
| 52 | AOSC = "" |
| 53 | Arch = "" |
| 54 | Artix = "" |
| 55 | CentOS = "" |
| 56 | Debian = "" |
| 57 | Redhat = "" |
| 58 | RedHatEnterprise = "" |
| 59 | |
| 60 | [username] |
| 61 | show_always = true |
| 62 | style_user = "bg:red fg:crust" |
| 63 | style_root = "bg:red fg:crust" |
| 64 | format = '[ $user]($style)' |
| 65 | |
| 66 | [directory] |
| 67 | style = "bg:peach fg:crust" |
| 68 | format = "[ $path ]($style)" |
| 69 | truncation_length = 3 |
| 70 | truncation_symbol = "…/" |
| 71 | |
| 72 | [directory.substitutions] |
| 73 | "Documents" = " " |
| 74 | "Downloads" = " " |
| 75 | "Music" = " " |
| 76 | "Pictures" = " " |
| 77 | "Developer" = " " |
| 78 | |
| 79 | [git_branch] |
| 80 | symbol = "" |
| 81 | style = "bg:yellow" |
| 82 | format = '[[ $symbol $branch ](fg:crust bg:yellow)]($style)' |
| 83 | |
| 84 | [git_status] |
| 85 | style = "bg:yellow" |
| 86 | format = '[[($all_status$ahead_behind )](fg:crust bg:yellow)]($style)' |
| 87 | |
| 88 | [nodejs] |
| 89 | symbol = "" |
| 90 | style = "bg:green" |
| 91 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 92 | |
| 93 | [bun] |
| 94 | symbol = "" |
| 95 | style = "bg:green" |
| 96 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 97 | |
| 98 | [c] |
| 99 | symbol = " " |
| 100 | style = "bg:green" |
| 101 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 102 | |
| 103 | [rust] |
| 104 | symbol = "" |
| 105 | style = "bg:green" |
| 106 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 107 | |
| 108 | [golang] |
| 109 | symbol = "" |
| 110 | style = "bg:green" |
| 111 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 112 | |
| 113 | [php] |
| 114 | symbol = "" |
| 115 | style = "bg:green" |
| 116 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 117 | |
| 118 | [java] |
| 119 | symbol = " " |
| 120 | style = "bg:green" |
| 121 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 122 | |
| 123 | [kotlin] |
| 124 | symbol = "" |
| 125 | style = "bg:green" |
| 126 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 127 | |
| 128 | [haskell] |
| 129 | symbol = "" |
| 130 | style = "bg:green" |
| 131 | format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' |
| 132 | |
| 133 | [python] |
| 134 | symbol = "" |
| 135 | style = "bg:green" |
| 136 | format = '[[ $symbol( $version)(\(#$virtualenv\)) ](fg:crust bg:green)]($style)' |
| 137 | |
| 138 | [docker_context] |
| 139 | symbol = "" |
| 140 | style = "bg:sapphire" |
| 141 | format = '[[ $symbol( $context) ](fg:crust bg:sapphire)]($style)' |
| 142 | |
| 143 | [conda] |
| 144 | symbol = " " |
| 145 | style = "fg:crust bg:sapphire" |
| 146 | format = '[$symbol$environment ]($style)' |
| 147 | ignore_base = false |
| 148 | |
| 149 | [time] |
| 150 | disabled = false |
| 151 | time_format = "%R" |
| 152 | style = "bg:lavender" |
| 153 | format = '[[ $time ](fg:crust bg:lavender)]($style)' |
| 154 | |
| 155 | [line_break] |
| 156 | disabled = false |
| 157 | |
| 158 | [character] |
| 159 | disabled = false |
| 160 | success_symbol = '[❯](bold fg:green)' |
| 161 | error_symbol = '[❯](bold fg:red)' |
| 162 | vimcmd_symbol = '[❮](bold fg:green)' |
| 163 | vimcmd_replace_one_symbol = '[❮](bold fg:lavender)' |
| 164 | vimcmd_replace_symbol = '[❮](bold fg:lavender)' |
| 165 | vimcmd_visual_symbol = '[❮](bold fg:yellow)' |
| 166 | |
| 167 | [cmd_duration] |
| 168 | show_milliseconds = true |
| 169 | format = " in $duration " |
| 170 | style = "bg:lavender" |
| 171 | disabled = false |
| 172 | show_notifications = true |
| 173 | min_time_to_notify = 45000 |
| 174 | |
| 175 | [palettes.catppuccin_mocha] |
| 176 | rosewater = "#f5e0dc" |
| 177 | flamingo = "#f2cdcd" |
| 178 | pink = "#f5c2e7" |
| 179 | mauve = "#cba6f7" |
| 180 | red = "#f38ba8" |
| 181 | maroon = "#eba0ac" |
| 182 | peach = "#fab387" |
| 183 | yellow = "#f9e2af" |
| 184 | green = "#a6e3a1" |
| 185 | teal = "#94e2d5" |
| 186 | sky = "#89dceb" |
| 187 | sapphire = "#74c7ec" |
| 188 | blue = "#89b4fa" |
| 189 | lavender = "#b4befe" |
| 190 | text = "#cdd6f4" |
| 191 | subtext1 = "#bac2de" |
| 192 | subtext0 = "#a6adc8" |
| 193 | overlay2 = "#9399b2" |
| 194 | overlay1 = "#7f849c" |
| 195 | overlay0 = "#6c7086" |
| 196 | surface2 = "#585b70" |
| 197 | surface1 = "#45475a" |
| 198 | surface0 = "#313244" |
| 199 | base = "#1e1e2e" |
| 200 | mantle = "#181825" |
| 201 | crust = "#11111b" |
| 202 | |
| 203 | [palettes.catppuccin_frappe] |
| 204 | rosewater = "#f2d5cf" |
| 205 | flamingo = "#eebebe" |
| 206 | pink = "#f4b8e4" |
| 207 | mauve = "#ca9ee6" |
| 208 | red = "#e78284" |
| 209 | maroon = "#ea999c" |
| 210 | peach = "#ef9f76" |
| 211 | yellow = "#e5c890" |
| 212 | green = "#a6d189" |
| 213 | teal = "#81c8be" |
| 214 | sky = "#99d1db" |
| 215 | sapphire = "#85c1dc" |
| 216 | blue = "#8caaee" |
| 217 | lavender = "#babbf1" |
| 218 | text = "#c6d0f5" |
| 219 | subtext1 = "#b5bfe2" |
| 220 | subtext0 = "#a5adce" |
| 221 | overlay2 = "#949cbb" |
| 222 | overlay1 = "#838ba7" |
| 223 | overlay0 = "#737994" |
| 224 | surface2 = "#626880" |
| 225 | surface1 = "#51576d" |
| 226 | surface0 = "#414559" |
| 227 | base = "#303446" |
| 228 | mantle = "#292c3c" |
| 229 | crust = "#232634" |
| 230 | |
| 231 | [palettes.catppuccin_latte] |
| 232 | rosewater = "#dc8a78" |
| 233 | flamingo = "#dd7878" |
| 234 | pink = "#ea76cb" |
| 235 | mauve = "#8839ef" |
| 236 | red = "#d20f39" |
| 237 | maroon = "#e64553" |
| 238 | peach = "#fe640b" |
| 239 | yellow = "#df8e1d" |
| 240 | green = "#40a02b" |
| 241 | teal = "#179299" |
| 242 | sky = "#04a5e5" |
| 243 | sapphire = "#209fb5" |
| 244 | blue = "#1e66f5" |
| 245 | lavender = "#7287fd" |
| 246 | text = "#4c4f69" |
| 247 | subtext1 = "#5c5f77" |
| 248 | subtext0 = "#6c6f85" |
| 249 | overlay2 = "#7c7f93" |
| 250 | overlay1 = "#8c8fa1" |
| 251 | overlay0 = "#9ca0b0" |
| 252 | surface2 = "#acb0be" |
| 253 | surface1 = "#bcc0cc" |
| 254 | surface0 = "#ccd0da" |
| 255 | base = "#eff1f5" |
| 256 | mantle = "#e6e9ef" |
| 257 | crust = "#dce0e8" |
| 258 | |
| 259 | [palettes.catppuccin_macchiato] |
| 260 | rosewater = "#f4dbd6" |
| 261 | flamingo = "#f0c6c6" |
| 262 | pink = "#f5bde6" |
| 263 | mauve = "#c6a0f6" |
| 264 | red = "#ed8796" |
| 265 | maroon = "#ee99a0" |
| 266 | peach = "#f5a97f" |
| 267 | yellow = "#eed49f" |
| 268 | green = "#a6da95" |
| 269 | teal = "#8bd5ca" |
| 270 | sky = "#91d7e3" |
| 271 | sapphire = "#7dc4e4" |
| 272 | blue = "#8aadf4" |
| 273 | lavender = "#b7bdf8" |
| 274 | text = "#cad3f5" |
| 275 | subtext1 = "#b8c0e0" |
| 276 | subtext0 = "#a5adcb" |
| 277 | overlay2 = "#939ab7" |
| 278 | overlay1 = "#8087a2" |
| 279 | overlay0 = "#6e738d" |
| 280 | surface2 = "#5b6078" |
| 281 | surface1 = "#494d64" |
| 282 | surface0 = "#363a4f" |
| 283 | base = "#24273a" |
| 284 | mantle = "#1e2030" |
| 285 | crust = "#181926" |
| 286 |
| 1 | #!/usr/bin/env zsh |
| 2 | |
| 3 | set -u |
| 4 | |
| 5 | repo_dir="${0:A:h}" |
| 6 | source "$repo_dir/func" |
| 7 | |
| 8 | failures=0 |
| 9 | fixture_dir="$(mktemp -d "${TMPDIR:-/tmp}/plan build launcher.XXXXXX")" || exit 1 |
| 10 | source_dir="$(mktemp -d "${TMPDIR:-/tmp}/plan-build-source.XXXXXX")" || exit 1 |
| 11 | export PLAN_BUILD_CACHE_DIR="$fixture_dir/cache with spaces" |
| 12 | export PLAN_BUILD_BASE_URL="file://$source_dir" |
| 13 | export HOME="$fixture_dir/home with spaces" |
| 14 | PLAN_BUILD_SKILLS_DIR="$HOME/.claude/skills" |
| 15 | mkdir -p "$source_dir" "$PLAN_BUILD_SKILLS_DIR/plan-build" "$PLAN_BUILD_SKILLS_DIR/plan-build-architect" |
| 16 | print -r -- "old standard" > "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" |
| 17 | print -r -- "old architect" > "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md" |
| 18 | trap 'rm -rf -- "$fixture_dir" "$source_dir"' EXIT |
| 19 | |
| 20 | fail() { |
| 21 | print -u2 -- "FAIL: $1" |
| 22 | failures=$((failures + 1)) |
| 23 | } |
| 24 | |
| 25 | assert_equal() { |
| 26 | [[ "$1" = "$2" ]] || fail "$3 (expected '$1', got '$2')" |
| 27 | } |
| 28 | |
| 29 | assert_contains() { |
| 30 | [[ "$1" == *"$2"* ]] || fail "$3 (missing '$2')" |
| 31 | } |
| 32 | |
| 33 | assert_file_contains() { |
| 34 | command grep -Fq -- "$2" "$1" || fail "$3" |
| 35 | } |
| 36 | |
| 37 | assert_bundle_consistent() { |
| 38 | local release="$PLAN_BUILD_CACHE_DIR/current" |
| 39 | local script_generation skill_generation architect_generation |
| 40 | release="${release:A}" |
| 41 | [[ -L "$PLAN_BUILD_CACHE_DIR/current" ]] || fail "$1 current is a symlink" |
| 42 | [[ -d "$release" ]] || fail "$1 current resolves to a release" |
| 43 | script_generation="$(command sed -n 's/^# generation: //p' "$release/plan_build.zsh")" |
| 44 | skill_generation="$(command sed -n 's/ standard skill$//p' "$release/SKILL.md")" |
| 45 | architect_generation="$(command sed -n 's/ architect skill$//p' "$release/ARCHITECT.md")" |
| 46 | assert_equal "$script_generation" "$skill_generation" "$1 script/standard generation" |
| 47 | assert_equal "$script_generation" "$architect_generation" "$1 script/architect generation" |
| 48 | } |
| 49 | |
| 50 | assert_skill_links() { |
| 51 | [[ -L "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" ]] || fail "$1 standard path is symlink" |
| 52 | [[ -L "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md" ]] || fail "$1 architect path is symlink" |
| 53 | assert_equal "$PLAN_BUILD_CACHE_DIR/current/SKILL.md" \ |
| 54 | "$(command readlink "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md")" "$1 standard link target" |
| 55 | assert_equal "$PLAN_BUILD_CACHE_DIR/current/ARCHITECT.md" \ |
| 56 | "$(command readlink "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md")" "$1 architect link target" |
| 57 | } |
| 58 | |
| 59 | write_bundle() { |
| 60 | local generation="$1" |
| 61 | local exit_code="${2:-0}" |
| 62 | local marker_file="${3:-}" |
| 63 | print -r -- '#!/usr/bin/env zsh' > "$source_dir/plan_build.zsh" |
| 64 | print -r -- "# generation: $generation" >> "$source_dir/plan_build.zsh" |
| 65 | print -r -- "print -r -- \"$generation:\${(j:|:)@}\"" >> "$source_dir/plan_build.zsh" |
| 66 | print -r -- "print -r -- \"path:\${0}\"" >> "$source_dir/plan_build.zsh" |
| 67 | print -r -- 'print -r -- "skills:${PLAN_BUILD_SKILL_PATH}|${PLAN_BUILD_ARCHITECT_SKILL_PATH}"' >> "$source_dir/plan_build.zsh" |
| 68 | [[ -n "$marker_file" ]] && print -r -- ": > ${(q)marker_file}" >> "$source_dir/plan_build.zsh" |
| 69 | print -r -- "exit $exit_code" >> "$source_dir/plan_build.zsh" |
| 70 | print -r -- "$generation standard skill" > "$source_dir/SKILL.md" |
| 71 | print -r -- "$generation architect skill" > "$source_dir/ARCHITECT.md" |
| 72 | } |
| 73 | |
| 74 | run_plan_build() { |
| 75 | output="$(plan_build "$@" 2>&1)" |
| 76 | rc=$? |
| 77 | } |
| 78 | |
| 79 | write_bundle initial |
| 80 | run_plan_build alpha "two words" |
| 81 | assert_equal 0 "$rc" "initial migration status" |
| 82 | assert_contains "$output" "initial:alpha|two words" "argument forwarding" |
| 83 | assert_skill_links "initial migration" |
| 84 | assert_bundle_consistent "initial migration" |
| 85 | assert_file_contains "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" "initial standard" "initial standard content" |
| 86 | assert_file_contains "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md" "initial architect" "initial architect content" |
| 87 | execution_path="${${(f)output}[(r)path:*]#path:}" |
| 88 | [[ "$execution_path" == "$PLAN_BUILD_CACHE_DIR"/release.*/plan_build.zsh ]] || |
| 89 | fail "execution uses an immutable release path" |
| 90 | assert_contains "$output" "skills:${execution_path:h}/SKILL.md|${execution_path:h}/ARCHITECT.md" \ |
| 91 | "execution binds immutable skill paths" |
| 92 | |
| 93 | old_release="$PLAN_BUILD_CACHE_DIR/current" |
| 94 | old_release="${old_release:A}" |
| 95 | write_bundle replacement 23 |
| 96 | run_plan_build forwarded |
| 97 | assert_equal 23 "$rc" "canonical status forwarding" |
| 98 | assert_contains "$output" "replacement:forwarded" "replacement executes" |
| 99 | new_release="$PLAN_BUILD_CACHE_DIR/current" |
| 100 | new_release="${new_release:A}" |
| 101 | [[ "$new_release" != "$old_release" ]] || fail "refresh activates a unique release" |
| 102 | assert_skill_links "replacement" |
| 103 | assert_bundle_consistent "replacement" |
| 104 | |
| 105 | print -r -- 'if broken' > "$source_dir/plan_build.zsh" |
| 106 | print -r -- "bad standard skill" > "$source_dir/SKILL.md" |
| 107 | print -r -- "bad architect skill" > "$source_dir/ARCHITECT.md" |
| 108 | run_plan_build fallback |
| 109 | assert_equal 23 "$rc" "failed refresh fallback status" |
| 110 | assert_contains "$output" "Warning: Unable to refresh" "failed refresh warning" |
| 111 | assert_contains "$output" "replacement:fallback" "failed refresh executes current" |
| 112 | preserved_release="$PLAN_BUILD_CACHE_DIR/current" |
| 113 | preserved_release="${preserved_release:A}" |
| 114 | assert_equal "$new_release" "$preserved_release" "failed refresh preserves current" |
| 115 | assert_skill_links "failed refresh" |
| 116 | assert_bundle_consistent "failed refresh" |
| 117 | |
| 118 | export PLAN_BUILD_CURL_COMMAND="definitely-missing-plan-build-curl" |
| 119 | run_plan_build no-curl |
| 120 | assert_equal 23 "$rc" "missing curl fallback status" |
| 121 | assert_contains "$output" "curl is unavailable" "missing curl warning" |
| 122 | assert_contains "$output" "replacement:no-curl" "missing curl executes current" |
| 123 | rm -f "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md" |
| 124 | run_plan_build repaired-offline |
| 125 | assert_equal 23 "$rc" "offline skill repair status" |
| 126 | assert_skill_links "offline skill repair" |
| 127 | unset PLAN_BUILD_CURL_COMMAND |
| 128 | |
| 129 | # An already selected immutable script must not change beneath a concurrent caller. |
| 130 | ready_file="$fixture_dir/ready" |
| 131 | go_file="$fixture_dir/go" |
| 132 | first_output="$fixture_dir/first.out" |
| 133 | print -r -- '#!/usr/bin/env zsh' > "$source_dir/plan_build.zsh" |
| 134 | print -r -- '# generation: concurrent_first' >> "$source_dir/plan_build.zsh" |
| 135 | print -r -- 'print -r -- "concurrent_first:${(j:|:)@}"' >> "$source_dir/plan_build.zsh" |
| 136 | print -r -- ": > ${(q)ready_file}" >> "$source_dir/plan_build.zsh" |
| 137 | print -r -- "while [[ ! -e ${(q)go_file} ]]; do sleep 0.01; done" >> "$source_dir/plan_build.zsh" |
| 138 | print -r -- 'print -r -- "finished:first"' >> "$source_dir/plan_build.zsh" |
| 139 | print -r -- "concurrent_first standard skill" > "$source_dir/SKILL.md" |
| 140 | print -r -- "concurrent_first architect skill" > "$source_dir/ARCHITECT.md" |
| 141 | (plan_build one > "$first_output" 2>&1) & |
| 142 | first_pid=$! |
| 143 | for attempt in {1..500}; do |
| 144 | [[ -e "$ready_file" ]] && break |
| 145 | sleep 0.01 |
| 146 | done |
| 147 | [[ -e "$ready_file" ]] || fail "first concurrent call reached canonical execution" |
| 148 | write_bundle concurrent_second |
| 149 | run_plan_build two |
| 150 | assert_equal 0 "$rc" "second concurrent status" |
| 151 | assert_contains "$output" "concurrent_second:two" "second concurrent generation" |
| 152 | : > "$go_file" |
| 153 | wait "$first_pid" || fail "first concurrent status" |
| 154 | first_text="$(<"$first_output")" |
| 155 | assert_contains "$first_text" "concurrent_first:one" "first call retains selected release" |
| 156 | assert_contains "$first_text" "finished:first" "first immutable script finishes" |
| 157 | assert_bundle_consistent "concurrent final current" |
| 158 | assert_skill_links "concurrent final links" |
| 159 | |
| 160 | # Cancellation while refreshing must clean staging and never execute canonical code. |
| 161 | blocking_curl="$fixture_dir/blocking-curl" |
| 162 | cancel_started="$fixture_dir/cancel.started" |
| 163 | cancel_executed="$fixture_dir/cancel.executed" |
| 164 | write_bundle cancellation-marker 0 "$cancel_executed" |
| 165 | run_plan_build activate-cancellation-marker |
| 166 | assert_equal 0 "$rc" "cancellation marker bundle activation" |
| 167 | rm -f "$cancel_executed" |
| 168 | { |
| 169 | print -r -- '#!/bin/sh' |
| 170 | print -r -- "touch ${(q)cancel_started}" |
| 171 | print -r -- 'while :; do sleep 1; done' |
| 172 | } > "$blocking_curl" |
| 173 | chmod +x "$blocking_curl" |
| 174 | export PLAN_BUILD_CURL_COMMAND="$blocking_curl" |
| 175 | (plan_build cancelled > "$fixture_dir/cancel.out" 2>&1; cancel_rc=$?; print -r -- "$cancel_rc" > "$fixture_dir/cancel.rc"; exit "$cancel_rc") & |
| 176 | cancel_pid=$! |
| 177 | for attempt in {1..500}; do |
| 178 | [[ -e "$cancel_started" ]] && break |
| 179 | sleep 0.01 |
| 180 | done |
| 181 | kill -TERM "$cancel_pid" |
| 182 | wait "$cancel_pid" |
| 183 | cancel_wait_rc=$? |
| 184 | cancel_recorded_rc="" |
| 185 | [[ -s "$fixture_dir/cancel.rc" ]] && cancel_recorded_rc="$(<"$fixture_dir/cancel.rc")" |
| 186 | [[ "$cancel_wait_rc" = 143 || "$cancel_recorded_rc" = 143 ]] || |
| 187 | fail "TERM returns conventional status 143" |
| 188 | [[ ! -e "$cancel_executed" ]] || fail "TERM does not execute canonical plan-build" |
| 189 | unset PLAN_BUILD_CURL_COMMAND |
| 190 | |
| 191 | # A pre-existing lock file must not be mistaken for an active kernel lock. |
| 192 | print -r -- "stale contents" > "$PLAN_BUILD_CACHE_DIR/.activate.lock" |
| 193 | write_bundle stale-lock-recovery |
| 194 | run_plan_build recovered |
| 195 | assert_equal 0 "$rc" "stale activation lock recovery status" |
| 196 | assert_contains "$output" "stale-lock-recovery:recovered" "stale activation lock recovery execution" |
| 197 | |
| 198 | # Cancellation while canonical code runs must terminate the child promptly. |
| 199 | canonical_started="$fixture_dir/canonical.started" |
| 200 | canonical_finished="$fixture_dir/canonical.finished" |
| 201 | print -r -- '#!/usr/bin/env zsh' > "$source_dir/plan_build.zsh" |
| 202 | print -r -- '# generation: canonical-cancel' >> "$source_dir/plan_build.zsh" |
| 203 | print -r -- "trap 'exit 143' TERM" >> "$source_dir/plan_build.zsh" |
| 204 | print -r -- ": > ${(q)canonical_started}" >> "$source_dir/plan_build.zsh" |
| 205 | print -r -- 'while :; do sleep 1; done' >> "$source_dir/plan_build.zsh" |
| 206 | print -r -- ": > ${(q)canonical_finished}" >> "$source_dir/plan_build.zsh" |
| 207 | print -r -- "canonical-cancel standard skill" > "$source_dir/SKILL.md" |
| 208 | print -r -- "canonical-cancel architect skill" > "$source_dir/ARCHITECT.md" |
| 209 | (plan_build canonical-cancel > "$fixture_dir/canonical-cancel.out" 2>&1; print -r -- "$?" > "$fixture_dir/canonical-cancel.rc") & |
| 210 | canonical_pid=$! |
| 211 | for attempt in {1..500}; do |
| 212 | [[ -e "$canonical_started" ]] && break |
| 213 | sleep 0.01 |
| 214 | done |
| 215 | [[ -e "$canonical_started" ]] || fail "canonical cancellation reached execution" |
| 216 | kill -TERM "$canonical_pid" |
| 217 | wait "$canonical_pid" |
| 218 | assert_equal 143 "$(<"$fixture_dir/canonical-cancel.rc")" "canonical cancellation status" |
| 219 | [[ ! -e "$canonical_finished" ]] || fail "canonical child continued after TERM" |
| 220 | |
| 221 | temp_count="$(command find "$PLAN_BUILD_CACHE_DIR" -maxdepth 1 \ |
| 222 | \( -name '.stage.*' -o -name '.current.*' \) -print | command wc -l | command tr -d ' ')" |
| 223 | assert_equal 0 "$temp_count" "cache staging and temporary links cleaned" |
| 224 | skill_temp_count="$(command find "$PLAN_BUILD_SKILLS_DIR" \ |
| 225 | \( -name '*.link.*' -o -name '*.backup.*' \) -print | command wc -l | command tr -d ' ')" |
| 226 | assert_equal 0 "$skill_temp_count" "skill temporary links and backups cleaned" |
| 227 | |
| 228 | if (( failures )); then |
| 229 | exit 1 |
| 230 | fi |
| 231 | |
| 232 | print -- "PASS: plan_build launcher tests" |
| 233 |
| 1 | " Enable line numbers |
| 2 | set number |
| 3 | |
| 4 | " Enable relative line numbers |
| 5 | set relativenumber |
| 6 | |
| 7 | " Enable syntax highlighting |
| 8 | syntax on |
| 9 | |
| 10 | " Set colorscheme |
| 11 | colorscheme slate |
| 12 | |
| 13 | " Enable file type detection and plugins |
| 14 | filetype plugin indent on |
| 15 | |
| 16 | " Set the tab width to 4 spaces |
| 17 | set tabstop=4 |
| 18 | set shiftwidth=4 |
| 19 | set expandtab |
| 20 | |
| 21 | " Enable auto-indentation |
| 22 | set autoindent |
| 23 | set smartindent |
| 24 | |
| 25 | " Highlight current line |
| 26 | set cursorline |
| 27 | |
| 28 | " Show matching parentheses |
| 29 | set showmatch |
| 30 | |
| 31 | " Enable line wrapping |
| 32 | set wrap |
| 33 | |
| 34 | " Enable mouse support |
| 35 | set mouse=a |
| 36 | |
| 37 | " Use Wayland's clipboard tools when this Vim lacks native clipboard support |
| 38 | if !has('clipboard') && exists('v:clipproviders') |
| 39 | \ && executable('wl-copy') && executable('wl-paste') |
| 40 | function! s:WaylandCopy(register, type, lines) abort |
| 41 | let l:text = join(a:lines, "\n") |
| 42 | if a:type ==# 'V' |
| 43 | let l:text ..= "\n" |
| 44 | endif |
| 45 | call system('wl-copy', l:text) |
| 46 | endfunction |
| 47 | |
| 48 | function! s:WaylandPaste(register) abort |
| 49 | return ['', systemlist('wl-paste --no-newline')] |
| 50 | endfunction |
| 51 | |
| 52 | let v:clipproviders['wltools'] = { |
| 53 | \ 'copy': { |
| 54 | \ '+': function('s:WaylandCopy'), |
| 55 | \ '*': function('s:WaylandCopy') |
| 56 | \ }, |
| 57 | \ 'paste': { |
| 58 | \ '+': function('s:WaylandPaste'), |
| 59 | \ '*': function('s:WaylandPaste') |
| 60 | \ } |
| 61 | \ } |
| 62 | set clipmethod^=wltools |
| 63 | endif |
| 64 | set clipboard=unnamedplus |
| 65 | |
| 66 | " Disable swap file |
| 67 | set noswapfile |
| 68 | |
| 69 | " Enable incremental search |
| 70 | set incsearch |
| 71 | |
| 72 | " Ignore case in search |
| 73 | set ignorecase |
| 74 | |
| 75 | " Override ignorecase if search contains capital letters |
| 76 | set smartcase |
| 77 | |
| 78 | " Display line and column number of the cursor position |
| 79 | set ruler |
| 80 | |
| 81 | " Set the status line at the bottom |
| 82 | set laststatus=2 |
| 83 | |
| 84 | " Show command in bottom bar |
| 85 | set showcmd |
| 86 | |
| 87 | " Set command height |
| 88 | set cmdheight=2 |
| 89 | |
| 90 | " Set history lines |
| 91 | set history=1000 |
| 92 | |
| 93 | " Disable backup file |
| 94 | set nobackup |
| 95 | |
| 96 | " Enable persistent undo |
| 97 | set undofile |
| 98 | |
| 99 | " Set maximum number of undo levels |
| 100 | set undolevels=1000 |
| 101 | |
| 102 | " Set undo directory |
| 103 | if has("persistent_undo") |
| 104 | silent !mkdir ~/.vim/undodir > /dev/null 2>&1 |
| 105 | set undodir=~/.vim/undodir |
| 106 | endif |
| 107 | |
| 108 | " Set search highlighting |
| 109 | set hlsearch |
| 110 | |
| 111 | " Enable visual bell |
| 112 | set visualbell |
| 113 | |
| 114 | " Set default file encoding |
| 115 | set encoding=utf-8 |
| 116 | |
| 117 | " Set the leader key to space |
| 118 | let mapleader = " " |
| 119 | |
| 120 | " Map <Leader>w to save the file |
| 121 | nnoremap <Leader>w :w<CR> |
| 122 | |
| 123 | " Map <Leader>q to quit |
| 124 | nnoremap <Leader>q :q<CR> |
| 125 | |
| 126 | " Map <Leader>x to save and quit |
| 127 | nnoremap <Leader>x :wq<CR> |
| 128 | |
| 129 | " Enable folding |
| 130 | set foldmethod=syntax |
| 131 | set foldlevelstart=99 |
| 132 | |
| 133 | " Enable line wrapping at 80 characters |
| 134 | set textwidth=80 |
| 135 | set colorcolumn=80 |
| 136 | |
| 137 | " Add some basic key mappings |
| 138 | " Map jj to escape insert mode |
| 139 | inoremap jj <Esc> |
| 140 | |
| 141 | " Map <Leader>n to toggle line numbers |
| 142 | nnoremap <Leader>n :set number!<CR> |
| 143 | |
| 144 | " Map <Leader>r to toggle relative line numbers |
| 145 | nnoremap <Leader>r :set relativenumber!<CR> |
| 146 | |
| 147 | " Configure plugins (if you use a plugin manager like vim-plug) |
| 148 | " Example with vim-plug: |
| 149 | " call plug#begin('~/.vim/plugged') |
| 150 | " Plug 'tpope/vim-sensible' |
| 151 | " Plug 'preservim/nerdtree' |
| 152 | " Plug 'junegunn/fzf', { 'do': { -> fzf#install() } } |
| 153 | " Plug 'airblade/vim-gitgutter' |
| 154 | " call plug#end() |
| 155 | |
| 156 | " NERDTree key mappings |
| 157 | " nnoremap <C-n> :NERDTreeToggle<CR> |
| 158 | |
| 159 | " Enable automatic hard wrapping at textwidth (80 chars) |
| 160 | set formatoptions+=t " Auto-wrap text using textwidth |
| 161 | set formatoptions+=c " Auto-wrap comments using textwidth |
| 162 | set formatoptions+=r " Continue comments when pressing Enter |
| 163 | set formatoptions+=o " Continue comments when using 'o' or 'O' |
| 164 | set formatoptions+=q " Allow formatting of comments with 'gq' |
| 165 | set formatoptions+=n " Recognize numbered lists |
| 166 | set formatoptions+=l " Don't break lines that were already long |
| 167 | |
| 168 | " Enable syntax highlighting |
| 169 | syntax on |
| 170 | |
| 171 | " Increase memory limit for complex syntax parsing (Prevents E363) |
| 172 | set maxmempattern=20000 |
| 173 |
| 1 | #!/bin/bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # ============================= |
| 5 | # COLORS & LOGGING |
| 6 | # ============================= |
| 7 | RED='\033[0;31m' |
| 8 | GREEN='\033[0;32m' |
| 9 | YELLOW='\033[1;33m' |
| 10 | BLUE='\033[0;34m' |
| 11 | NC='\033[0m' |
| 12 | |
| 13 | log() { echo -e "${BLUE}[INFO]${NC} $*"; } |
| 14 | ok() { echo -e "${GREEN}[OK]${NC} $*"; } |
| 15 | warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } |
| 16 | err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } |
| 17 | |
| 18 | # ============================= |
| 19 | # CONFIG |
| 20 | # ============================= |
| 21 | GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD" |
| 22 | |
| 23 | CONFIG_FILES=( |
| 24 | ".alias" |
| 25 | ".func" |
| 26 | ".pathrc" |
| 27 | ".sourcerc" |
| 28 | ".vimrc" |
| 29 | ".zshrc" |
| 30 | ".config/starship.toml" |
| 31 | ) |
| 32 | |
| 33 | # ============================= |
| 34 | # REQUIREMENTS |
| 35 | # ============================= |
| 36 | check_requirements() { |
| 37 | if [[ $EUID -eq 0 ]]; then |
| 38 | err "Do not run as root on macOS" |
| 39 | exit 1 |
| 40 | fi |
| 41 | } |
| 42 | |
| 43 | # ============================= |
| 44 | # INSTALLATION FUNCTIONS |
| 45 | # ============================= |
| 46 | install_oh_my_zsh() { |
| 47 | log "Installing Oh My Zsh and plugins..." |
| 48 | export RUNZSH=no |
| 49 | export CHSH=no |
| 50 | export KEEP_ZSHRC=yes |
| 51 | |
| 52 | if [[ ! -d "$HOME/.oh-my-zsh" ]]; then |
| 53 | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended |
| 54 | else |
| 55 | ok "Oh My Zsh already installed" |
| 56 | fi |
| 57 | |
| 58 | local custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" |
| 59 | local plugin_dir="$custom/plugins" |
| 60 | mkdir -p "$plugin_dir" |
| 61 | |
| 62 | [[ -d "$plugin_dir/zsh-autosuggestions" ]] || \ |
| 63 | git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir/zsh-autosuggestions" |
| 64 | |
| 65 | [[ -d "$plugin_dir/zsh-syntax-highlighting" ]] || \ |
| 66 | git clone https://github.com/zsh-users/zsh-syntax-highlighting "$plugin_dir/zsh-syntax-highlighting" |
| 67 | |
| 68 | ok "Oh My Zsh plugins installed to $plugin_dir" |
| 69 | } |
| 70 | |
| 71 | install_starship() { |
| 72 | if command -v starship >/dev/null 2>&1 || [[ -f "$HOME/.local/bin/starship" ]]; then |
| 73 | ok "Starship already installed" |
| 74 | elif command -v brew >/dev/null 2>&1; then |
| 75 | log "Installing Starship with Homebrew..." |
| 76 | brew install starship |
| 77 | ok "Starship installed" |
| 78 | else |
| 79 | log "Installing Starship..." |
| 80 | mkdir -p "$HOME/.local/bin" |
| 81 | curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin" |
| 82 | ok "Starship installed" |
| 83 | fi |
| 84 | } |
| 85 | |
| 86 | download_configs() { |
| 87 | log "Downloading custom config files..." |
| 88 | local backup="$HOME/.config_backup_$(date +%Y%m%d_%H%M%S)" |
| 89 | mkdir -p "$backup" |
| 90 | |
| 91 | for f in "${CONFIG_FILES[@]}"; do |
| 92 | # Strip the leading dot for the download URL |
| 93 | local remote_file="${f#.}" |
| 94 | [[ "$f" == ".config/starship.toml" ]] && remote_file="starship.toml" |
| 95 | local url="$GIST_RAW_BASE/$remote_file" |
| 96 | local target="$HOME/$f" |
| 97 | local tmp="${target}.tmp.$$" |
| 98 | |
| 99 | mkdir -p "$(dirname "$target")" |
| 100 | |
| 101 | if [[ -f "$target" ]]; then |
| 102 | cp "$target" "$backup/" |
| 103 | fi |
| 104 | |
| 105 | log "Fetching $remote_file -> $f ..." |
| 106 | if curl -fsSL "$url" -o "$tmp"; then |
| 107 | mv "$tmp" "$target" |
| 108 | else |
| 109 | rm -f "$tmp" |
| 110 | warn "Failed to download $remote_file" |
| 111 | fi |
| 112 | done |
| 113 | |
| 114 | ok "Configs downloaded (Backup at $backup)" |
| 115 | } |
| 116 | |
| 117 | configure_zshrc() { |
| 118 | local zshrc="$HOME/.zshrc" |
| 119 | log "Configuring .zshrc for Oh My Zsh and Starship..." |
| 120 | |
| 121 | touch "$zshrc" |
| 122 | cp "$zshrc" "$HOME/.zshrc.backup_$(date +%Y%m%d_%H%M%S)" |
| 123 | |
| 124 | cat > "$zshrc" <<'EOF' |
| 125 | # ============================================================================= |
| 126 | # 1. HELPER FUNCTIONS & PATH |
| 127 | # ============================================================================= |
| 128 | source_if_readable() { |
| 129 | local file="$1" |
| 130 | if [[ -f "$file" && -r "$file" ]]; then |
| 131 | source "$file" |
| 132 | fi |
| 133 | } |
| 134 | |
| 135 | export PATH="$HOME/.local/bin:$PATH" |
| 136 | |
| 137 | # ============================================================================= |
| 138 | # 2. OH MY ZSH FRAMEWORK |
| 139 | # ============================================================================= |
| 140 | export ZSH="${ZSH:-$HOME/.oh-my-zsh}" |
| 141 | ZSH_THEME="" |
| 142 | |
| 143 | plugins=( |
| 144 | git |
| 145 | zsh-autosuggestions |
| 146 | zsh-syntax-highlighting |
| 147 | ) |
| 148 | |
| 149 | source_if_readable "$ZSH/oh-my-zsh.sh" |
| 150 | |
| 151 | # ============================================================================= |
| 152 | # 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS |
| 153 | # ============================================================================= |
| 154 | source_if_readable "$HOME/.sourcerc" |
| 155 | source_if_readable "$HOME/.func" |
| 156 | source_if_readable "$HOME/.pathrc" |
| 157 | source_if_readable "$HOME/.alias" |
| 158 | |
| 159 | # ============================================================================= |
| 160 | # 4. STARSHIP PROMPT |
| 161 | # ============================================================================= |
| 162 | if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then |
| 163 | export STARSHIP_CONFIG="$HOME/.config/starship.toml" |
| 164 | fi |
| 165 | |
| 166 | if command -v starship >/dev/null 2>&1; then |
| 167 | eval "$(starship init zsh)" |
| 168 | elif [[ -x "$HOME/.local/bin/starship" ]]; then |
| 169 | eval "$("$HOME/.local/bin/starship" init zsh)" |
| 170 | fi |
| 171 | EOF |
| 172 | |
| 173 | ok ".zshrc configured for Oh My Zsh framework with Starship prompt" |
| 174 | } |
| 175 | |
| 176 | configure_git_identity() { |
| 177 | log "Configuring global Git identity..." |
| 178 | git config --global user.name "Vernon Wee Hong KOH" |
| 179 | git config --global user.email "261549452+weehong@users.noreply.github.com" |
| 180 | ok "Global Git identity configured" |
| 181 | } |
| 182 | |
| 183 | switch_shell() { |
| 184 | log "Starting Zsh session..." |
| 185 | echo -e "${YELLOW}Type 'exit' to return to this installer menu.${NC}" |
| 186 | echo "----------------------------------------" |
| 187 | zsh -l |
| 188 | echo "----------------------------------------" |
| 189 | ok "Returned from Zsh session" |
| 190 | } |
| 191 | |
| 192 | # ============================= |
| 193 | # INTERACTIVE MENU |
| 194 | # ============================= |
| 195 | show_menu() { |
| 196 | echo "===========================================" |
| 197 | echo "macOS Minimal Zsh Setup - Choose what to do" |
| 198 | echo "===========================================" |
| 199 | echo " 0) Run ALL steps (1-6)" |
| 200 | echo " 1) Install Oh My Zsh + plugins" |
| 201 | echo " 2) Install Starship prompt" |
| 202 | echo " 3) Download custom configs (~/.alias, .func, .vimrc, etc.)" |
| 203 | echo " 4) Configure ~/.zshrc (Oh My Zsh + Starship)" |
| 204 | echo " 5) Configure global Git identity" |
| 205 | echo " 6) Switch to Zsh (Temporary Sub-shell)" |
| 206 | echo " 7) Quit" |
| 207 | echo "===========================================" |
| 208 | } |
| 209 | |
| 210 | run_choices() { |
| 211 | local input |
| 212 | read -p "Select: " input |
| 213 | input="${input//,/ }" |
| 214 | |
| 215 | local -a to_run=() |
| 216 | local -a to_exclude=() |
| 217 | |
| 218 | for item in $input; do |
| 219 | if [[ "$item" == !* ]]; then |
| 220 | to_exclude+=("${item:1}") |
| 221 | elif [[ "$item" == "0" ]]; then |
| 222 | to_run+=(1 2 3 4 5 6) |
| 223 | else |
| 224 | to_run+=("$item") |
| 225 | fi |
| 226 | done |
| 227 | |
| 228 | if [[ ${#to_run[@]} -gt 0 ]]; then |
| 229 | for choice in "${to_run[@]}"; do |
| 230 | local skip=false |
| 231 | |
| 232 | if [[ ${#to_exclude[@]} -gt 0 ]]; then |
| 233 | for ex in "${to_exclude[@]}"; do |
| 234 | if [[ "$choice" == "$ex" ]]; then |
| 235 | skip=true |
| 236 | break |
| 237 | fi |
| 238 | done |
| 239 | fi |
| 240 | |
| 241 | $skip && continue |
| 242 | |
| 243 | case "$choice" in |
| 244 | 1) install_oh_my_zsh ;; |
| 245 | 2) install_starship ;; |
| 246 | 3) download_configs ;; |
| 247 | 4) configure_zshrc ;; |
| 248 | 5) configure_git_identity ;; |
| 249 | 6) switch_shell ;; |
| 250 | 7) exit 0 ;; |
| 251 | *) warn "Skipping invalid option: $choice" ;; |
| 252 | esac |
| 253 | echo |
| 254 | done |
| 255 | fi |
| 256 | } |
| 257 | |
| 258 | # ============================= |
| 259 | # MAIN |
| 260 | # ============================= |
| 261 | main() { |
| 262 | check_requirements |
| 263 | while true; do |
| 264 | show_menu |
| 265 | run_choices |
| 266 | read -p "Do you want to run more options? (y/n): " again |
| 267 | [[ "$again" =~ ^[Yy]$ ]] || break |
| 268 | done |
| 269 | ok "macOS minimal Zsh configuration complete!" |
| 270 | } |
| 271 | |
| 272 | main "$@" |
| 273 |
| 1 | #!/bin/bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # ============================= |
| 5 | # COLORS & LOGGING |
| 6 | # ============================= |
| 7 | RED='\033[0;31m' |
| 8 | GREEN='\033[0;32m' |
| 9 | YELLOW='\033[1;33m' |
| 10 | BLUE='\033[0;34m' |
| 11 | NC='\033[0m' |
| 12 | |
| 13 | log() { echo -e "${BLUE}[INFO]${NC} $*"; } |
| 14 | ok() { echo -e "${GREEN}[OK]${NC} $*"; } |
| 15 | warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } |
| 16 | err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } |
| 17 | |
| 18 | # ============================= |
| 19 | # FLAGS & CONFIG |
| 20 | # ============================= |
| 21 | SKIP_PACKAGES=false |
| 22 | SKIP_SHELL_CHANGE=false |
| 23 | INSTALL_HOMEBREW=false |
| 24 | |
| 25 | GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD" |
| 26 | CONFIG_FILES=( |
| 27 | ".alias" |
| 28 | ".func" |
| 29 | ".pathrc" |
| 30 | ".sourcerc" |
| 31 | ".vimrc" |
| 32 | ".zshrc" |
| 33 | ".config/starship.toml" |
| 34 | ) |
| 35 | |
| 36 | # ============================= |
| 37 | # OS DETECTION |
| 38 | # ============================= |
| 39 | detect_os() { |
| 40 | if [[ "$OSTYPE" == "darwin"* ]]; then |
| 41 | echo "macos" |
| 42 | elif [ -f /etc/os-release ]; then |
| 43 | . /etc/os-release |
| 44 | echo "$ID" |
| 45 | else |
| 46 | echo "unknown" |
| 47 | fi |
| 48 | } |
| 49 | |
| 50 | # ============================= |
| 51 | # REQUIREMENTS |
| 52 | # ============================= |
| 53 | check_requirements() { |
| 54 | if [[ $EUID -eq 0 ]]; then |
| 55 | err "Do not run as root. The script will request sudo when necessary." |
| 56 | exit 1 |
| 57 | fi |
| 58 | if [[ "$(detect_os)" != "macos" ]] && ! command -v sudo >/dev/null 2>&1; then |
| 59 | err "sudo required on Linux" |
| 60 | exit 1 |
| 61 | fi |
| 62 | } |
| 63 | |
| 64 | # ============================= |
| 65 | # INSTALLATION FUNCTIONS |
| 66 | # ============================= |
| 67 | update_system() { |
| 68 | $SKIP_PACKAGES && return |
| 69 | log "Updating system..." |
| 70 | local os=$(detect_os) |
| 71 | |
| 72 | case "$os" in |
| 73 | macos) |
| 74 | brew update || warn "Homebrew update failed; continuing installer" |
| 75 | ;; |
| 76 | ubuntu|debian) |
| 77 | sudo apt-get update -y |
| 78 | if ! sudo apt-get upgrade -y; then |
| 79 | warn "System upgrade did not complete. This can happen when apt wants to downgrade a package." |
| 80 | warn "Continuing because the Zsh setup does not require OS package upgrades to finish." |
| 81 | fi |
| 82 | ;; |
| 83 | fedora) |
| 84 | sudo dnf upgrade -y || warn "System upgrade failed; continuing installer" |
| 85 | ;; |
| 86 | arch) |
| 87 | sudo pacman -Syu --noconfirm || warn "System upgrade failed; continuing installer" |
| 88 | ;; |
| 89 | *) warn "Auto-update not supported for OS: $os" ;; |
| 90 | esac |
| 91 | ok "System update step finished" |
| 92 | } |
| 93 | |
| 94 | install_packages() { |
| 95 | $SKIP_PACKAGES && return |
| 96 | log "Installing core packages..." |
| 97 | local os=$(detect_os) |
| 98 | |
| 99 | case "$os" in |
| 100 | macos) brew install zsh git vim curl wget unzip xz ;; |
| 101 | ubuntu|debian) sudo apt-get install -y zsh git vim curl wget unzip zip build-essential xz-utils ;; |
| 102 | fedora) sudo dnf install -y zsh git vim curl wget unzip zip @development-tools xz ;; |
| 103 | arch) sudo pacman -S --noconfirm zsh git vim curl wget unzip zip base-devel xz ;; |
| 104 | *) warn "Auto-install not supported for OS: $os. Please install zsh, git, vim, curl manually." ;; |
| 105 | esac |
| 106 | ok "Packages installed" |
| 107 | } |
| 108 | |
| 109 | set_timezone() { |
| 110 | log "Setting timezone to Asia/Singapore..." |
| 111 | local os=$(detect_os) |
| 112 | if [[ "$os" == "macos" ]]; then |
| 113 | sudo systemsetup -settimezone Asia/Singapore >/dev/null |
| 114 | else |
| 115 | sudo timedatectl set-timezone Asia/Singapore |
| 116 | fi |
| 117 | ok "Timezone set to Asia/Singapore" |
| 118 | } |
| 119 | |
| 120 | install_homebrew() { |
| 121 | ! $INSTALL_HOMEBREW && return |
| 122 | command -v brew >/dev/null 2>&1 && ok "Homebrew already installed" && return |
| 123 | log "Installing Homebrew..." |
| 124 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
| 125 | ok "Homebrew installed" |
| 126 | } |
| 127 | |
| 128 | configure_shell() { |
| 129 | $SKIP_SHELL_CHANGE && return |
| 130 | log "Changing default shell to zsh..." |
| 131 | local zsh_path |
| 132 | zsh_path="$(command -v zsh)" |
| 133 | |
| 134 | if ! grep -qx "$zsh_path" /etc/shells; then |
| 135 | echo "$zsh_path" | sudo tee -a /etc/shells >/dev/null |
| 136 | fi |
| 137 | chsh -s "$zsh_path" |
| 138 | ok "Shell changed (requires logout/login to take effect)" |
| 139 | } |
| 140 | |
| 141 | install_oh_my_zsh() { |
| 142 | log "Installing Oh My Zsh and plugins..." |
| 143 | export RUNZSH=no |
| 144 | export CHSH=no |
| 145 | export KEEP_ZSHRC=yes |
| 146 | |
| 147 | if [[ ! -d "$HOME/.oh-my-zsh" ]]; then |
| 148 | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended |
| 149 | else |
| 150 | ok "Oh My Zsh already installed" |
| 151 | fi |
| 152 | |
| 153 | local custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" |
| 154 | local plugin_dir="$custom/plugins" |
| 155 | mkdir -p "$plugin_dir" |
| 156 | |
| 157 | [[ -d "$plugin_dir/zsh-autosuggestions" ]] || git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir/zsh-autosuggestions" |
| 158 | [[ -d "$plugin_dir/zsh-syntax-highlighting" ]] || git clone https://github.com/zsh-users/zsh-syntax-highlighting "$plugin_dir/zsh-syntax-highlighting" |
| 159 | |
| 160 | ok "Oh My Zsh plugins installed to $plugin_dir" |
| 161 | } |
| 162 | |
| 163 | install_starship() { |
| 164 | if command -v starship >/dev/null 2>&1 || [[ -f "$HOME/.local/bin/starship" ]]; then |
| 165 | ok "Starship already installed" |
| 166 | else |
| 167 | log "Installing Starship prompt..." |
| 168 | if command -v brew >/dev/null 2>&1; then |
| 169 | brew install starship |
| 170 | else |
| 171 | mkdir -p "$HOME/.local/bin" |
| 172 | curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin" |
| 173 | fi |
| 174 | ok "Starship installed" |
| 175 | fi |
| 176 | } |
| 177 | |
| 178 | download_configs() { |
| 179 | log "Downloading custom config files from OpenGist..." |
| 180 | local backup="$HOME/.config_backup_$(date +%Y%m%d_%H%M%S)" |
| 181 | mkdir -p "$backup" |
| 182 | |
| 183 | for f in "${CONFIG_FILES[@]}"; do |
| 184 | local remote_file="${f#.}" |
| 185 | [[ "$f" == ".config/starship.toml" ]] && remote_file="starship.toml" |
| 186 | local url="$GIST_RAW_BASE/$remote_file" |
| 187 | local target="$HOME/$f" |
| 188 | local tmp="${target}.tmp.$$" |
| 189 | |
| 190 | mkdir -p "$(dirname "$target")" |
| 191 | |
| 192 | if [[ -f "$target" ]]; then |
| 193 | cp "$target" "$backup/" |
| 194 | fi |
| 195 | |
| 196 | log "Fetching $remote_file -> $f ..." |
| 197 | if curl -fsSL "$url" -o "$tmp"; then |
| 198 | mv "$tmp" "$target" |
| 199 | else |
| 200 | rm -f "$tmp" |
| 201 | warn "Failed to download $remote_file" |
| 202 | fi |
| 203 | done |
| 204 | |
| 205 | ok "Configs downloaded (Backup at $backup)" |
| 206 | } |
| 207 | |
| 208 | configure_zshrc() { |
| 209 | local zshrc="$HOME/.zshrc" |
| 210 | log "Configuring .zshrc for Oh My Zsh and Starship..." |
| 211 | |
| 212 | touch "$zshrc" |
| 213 | cp "$zshrc" "$HOME/.zshrc.backup_$(date +%Y%m%d_%H%M%S)" |
| 214 | |
| 215 | cat > "$zshrc" <<'EOF' |
| 216 | # ============================================================================= |
| 217 | # 1. HELPER FUNCTIONS & PATH |
| 218 | # ============================================================================= |
| 219 | source_if_readable() { |
| 220 | local file="$1" |
| 221 | if [[ -f "$file" && -r "$file" ]]; then |
| 222 | source "$file" |
| 223 | fi |
| 224 | } |
| 225 | |
| 226 | export PATH="$HOME/.local/bin:$PATH" |
| 227 | |
| 228 | # ============================================================================= |
| 229 | # 2. OH MY ZSH FRAMEWORK |
| 230 | # ============================================================================= |
| 231 | export ZSH="${ZSH:-$HOME/.oh-my-zsh}" |
| 232 | ZSH_THEME="" |
| 233 | |
| 234 | plugins=( |
| 235 | git |
| 236 | zsh-autosuggestions |
| 237 | zsh-syntax-highlighting |
| 238 | ) |
| 239 | |
| 240 | source_if_readable "$ZSH/oh-my-zsh.sh" |
| 241 | |
| 242 | # ============================================================================= |
| 243 | # 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS |
| 244 | # ============================================================================= |
| 245 | source_if_readable "$HOME/.sourcerc" |
| 246 | source_if_readable "$HOME/.func" |
| 247 | source_if_readable "$HOME/.pathrc" |
| 248 | source_if_readable "$HOME/.alias" |
| 249 | |
| 250 | # ============================================================================= |
| 251 | # 4. STARSHIP PROMPT |
| 252 | # ============================================================================= |
| 253 | if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then |
| 254 | export STARSHIP_CONFIG="$HOME/.config/starship.toml" |
| 255 | fi |
| 256 | |
| 257 | if command -v starship >/dev/null 2>&1; then |
| 258 | eval "$(starship init zsh)" |
| 259 | elif [[ -x "$HOME/.local/bin/starship" ]]; then |
| 260 | eval "$("$HOME/.local/bin/starship" init zsh)" |
| 261 | fi |
| 262 | EOF |
| 263 | |
| 264 | ok ".zshrc configured for Oh My Zsh framework with Starship prompt" |
| 265 | } |
| 266 | |
| 267 | configure_git_identity() { |
| 268 | log "Configuring global Git identity..." |
| 269 | git config --global user.name "Vernon Wee Hong KOH" |
| 270 | git config --global user.email "261549452+weehong@users.noreply.github.com" |
| 271 | ok "Global Git identity configured" |
| 272 | } |
| 273 | |
| 274 | switch_shell() { |
| 275 | log "Starting Zsh session..." |
| 276 | echo -e "${YELLOW}Type 'exit' to return to this installer menu.${NC}" |
| 277 | echo "----------------------------------------" |
| 278 | zsh -l |
| 279 | echo "----------------------------------------" |
| 280 | ok "Returned from Zsh session" |
| 281 | } |
| 282 | |
| 283 | # ============================= |
| 284 | # INTERACTIVE MENU |
| 285 | # ============================= |
| 286 | show_menu() { |
| 287 | echo "===========================================" |
| 288 | echo "Minimal Zsh Installer - Choose what to do" |
| 289 | echo "===========================================" |
| 290 | echo " 0) Run ALL steps (1-11)" |
| 291 | echo " 1) Update system packages" |
| 292 | echo " 2) Install core packages (zsh, git, vim, etc.)" |
| 293 | echo " 3) Set Timezone (Asia/Singapore)" |
| 294 | echo " 4) Install Homebrew" |
| 295 | echo " 5) Configure shell (chsh - sets default shell)" |
| 296 | echo " 6) Install Oh My Zsh + plugins" |
| 297 | echo " 7) Install Starship prompt" |
| 298 | echo " 8) Download custom configs (from OpenGist)" |
| 299 | echo " 9) Configure ~/.zshrc (Oh My Zsh + Starship)" |
| 300 | echo "10) Configure global Git identity" |
| 301 | echo "11) Switch to Zsh (Temporary Sub-shell)" |
| 302 | echo "12) Quit" |
| 303 | echo "===========================================" |
| 304 | } |
| 305 | |
| 306 | run_choices() { |
| 307 | local input |
| 308 | read -p "Select: " input |
| 309 | input="${input//,/ }" |
| 310 | |
| 311 | local -a to_run=() |
| 312 | local -a to_exclude=() |
| 313 | |
| 314 | for item in $input; do |
| 315 | if [[ "$item" == !* ]]; then |
| 316 | to_exclude+=("${item:1}") |
| 317 | elif [[ "$item" == "0" ]]; then |
| 318 | to_run+=(1 2 3 4 5 6 7 8 9 10 11) |
| 319 | else |
| 320 | to_run+=("$item") |
| 321 | fi |
| 322 | done |
| 323 | |
| 324 | for choice in "${to_run[@]}"; do |
| 325 | local skip=false |
| 326 | |
| 327 | for ex in "${to_exclude[@]}"; do |
| 328 | if [[ "$choice" == "$ex" ]]; then |
| 329 | skip=true |
| 330 | break |
| 331 | fi |
| 332 | done |
| 333 | |
| 334 | $skip && continue |
| 335 | |
| 336 | case "$choice" in |
| 337 | 1) update_system ;; |
| 338 | 2) install_packages ;; |
| 339 | 3) set_timezone ;; |
| 340 | 4) install_homebrew ;; |
| 341 | 5) configure_shell ;; |
| 342 | 6) install_oh_my_zsh ;; |
| 343 | 7) install_starship ;; |
| 344 | 8) download_configs ;; |
| 345 | 9) configure_zshrc ;; |
| 346 | 10) configure_git_identity ;; |
| 347 | 11) switch_shell ;; |
| 348 | 12) log "Exiting..."; exit 0 ;; |
| 349 | *) warn "Skipping invalid option: $choice" ;; |
| 350 | esac |
| 351 | echo |
| 352 | done |
| 353 | } |
| 354 | |
| 355 | # ============================= |
| 356 | # MAIN |
| 357 | # ============================= |
| 358 | main() { |
| 359 | check_requirements |
| 360 | while true; do |
| 361 | show_menu |
| 362 | run_choices |
| 363 | read -p "Do you want to run more options? (y/n): " again |
| 364 | [[ "$again" =~ ^[Yy]$ ]] || break |
| 365 | done |
| 366 | ok "Zsh installation/configuration complete!" |
| 367 | } |
| 368 | |
| 369 | main "$@" |
| 370 |
| 1 | #!/bin/bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # ============================= |
| 5 | # COLORS & LOGGING |
| 6 | # ============================= |
| 7 | RED='\033[0;31m' |
| 8 | GREEN='\033[0;32m' |
| 9 | YELLOW='\033[1;33m' |
| 10 | BLUE='\033[0;34m' |
| 11 | NC='\033[0m' |
| 12 | |
| 13 | log() { echo -e "${BLUE}[INFO]${NC} $*"; } |
| 14 | ok() { echo -e "${GREEN}[OK]${NC} $*"; } |
| 15 | warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } |
| 16 | err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } |
| 17 | |
| 18 | # ============================= |
| 19 | # FLAGS & CONFIG |
| 20 | # ============================= |
| 21 | SKIP_PACKAGES=false |
| 22 | SKIP_SHELL_CHANGE=false |
| 23 | INSTALL_HOMEBREW=false |
| 24 | |
| 25 | GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD" |
| 26 | CONFIG_FILES=( |
| 27 | ".alias" |
| 28 | ".func" |
| 29 | ".pathrc" |
| 30 | ".sourcerc" |
| 31 | ".vimrc" |
| 32 | ".zshrc" |
| 33 | ".config/starship.toml" |
| 34 | ) |
| 35 | |
| 36 | # ============================= |
| 37 | # PLATFORM DETECTION |
| 38 | # ============================= |
| 39 | detect_os() { |
| 40 | if [ -f /etc/os-release ]; then |
| 41 | . /etc/os-release |
| 42 | echo "$ID" |
| 43 | else |
| 44 | echo "unknown" |
| 45 | fi |
| 46 | } |
| 47 | |
| 48 | is_wsl() { |
| 49 | grep -qi "microsoft" /proc/version 2>/dev/null |
| 50 | } |
| 51 | |
| 52 | # ============================= |
| 53 | # REQUIREMENTS |
| 54 | # ============================= |
| 55 | check_requirements() { |
| 56 | if [[ $EUID -eq 0 ]]; then |
| 57 | err "Do not run as root. The script will request sudo when necessary." |
| 58 | exit 1 |
| 59 | fi |
| 60 | if ! command -v sudo >/dev/null 2>&1; then |
| 61 | err "sudo required" |
| 62 | exit 1 |
| 63 | fi |
| 64 | if ! is_wsl; then |
| 65 | err "This installer is intended for WSL" |
| 66 | exit 1 |
| 67 | fi |
| 68 | } |
| 69 | |
| 70 | # ============================= |
| 71 | # INSTALLATION FUNCTIONS |
| 72 | # ============================= |
| 73 | update_system() { |
| 74 | $SKIP_PACKAGES && return |
| 75 | log "Updating system..." |
| 76 | local os=$(detect_os) |
| 77 | |
| 78 | case "$os" in |
| 79 | ubuntu|debian) |
| 80 | sudo apt-get update -y |
| 81 | if ! sudo apt-get upgrade -y; then |
| 82 | warn "System upgrade did not complete. This can happen when apt wants to downgrade a package." |
| 83 | warn "Continuing because the Zsh setup does not require OS package upgrades to finish." |
| 84 | fi |
| 85 | ;; |
| 86 | fedora) |
| 87 | sudo dnf upgrade -y || warn "System upgrade failed; continuing installer" |
| 88 | ;; |
| 89 | arch) |
| 90 | sudo pacman -Syu --noconfirm || warn "System upgrade failed; continuing installer" |
| 91 | ;; |
| 92 | *) warn "Auto-update not supported for OS: $os" ;; |
| 93 | esac |
| 94 | ok "System update step finished" |
| 95 | } |
| 96 | |
| 97 | install_packages() { |
| 98 | $SKIP_PACKAGES && return |
| 99 | log "Installing core packages..." |
| 100 | local os=$(detect_os) |
| 101 | |
| 102 | case "$os" in |
| 103 | ubuntu|debian) sudo apt-get install -y zsh git vim curl wget unzip zip build-essential xz-utils ;; |
| 104 | fedora) sudo dnf install -y zsh git vim curl wget unzip zip @development-tools xz ;; |
| 105 | arch) sudo pacman -S --noconfirm zsh git vim curl wget unzip zip base-devel xz ;; |
| 106 | *) warn "Auto-install not supported for OS: $os. Please install zsh, git, vim, curl manually." ;; |
| 107 | esac |
| 108 | ok "Packages installed" |
| 109 | } |
| 110 | |
| 111 | set_timezone() { |
| 112 | log "Checking timezone configuration..." |
| 113 | if command -v timedatectl >/dev/null 2>&1 && timedatectl status >/dev/null 2>&1; then |
| 114 | sudo timedatectl set-timezone Asia/Singapore |
| 115 | ok "Timezone set to Asia/Singapore" |
| 116 | return |
| 117 | fi |
| 118 | |
| 119 | warn "Skipping timezone change: timedatectl is not available in this WSL environment" |
| 120 | } |
| 121 | |
| 122 | install_homebrew() { |
| 123 | ! $INSTALL_HOMEBREW && return |
| 124 | command -v brew >/dev/null 2>&1 && ok "Homebrew already installed" && return |
| 125 | log "Installing Homebrew..." |
| 126 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
| 127 | ok "Homebrew installed" |
| 128 | } |
| 129 | |
| 130 | configure_shell() { |
| 131 | $SKIP_SHELL_CHANGE && return |
| 132 | log "Changing default shell to zsh..." |
| 133 | local zsh_path |
| 134 | zsh_path="$(command -v zsh)" |
| 135 | |
| 136 | if ! grep -qx "$zsh_path" /etc/shells; then |
| 137 | echo "$zsh_path" | sudo tee -a /etc/shells >/dev/null |
| 138 | fi |
| 139 | chsh -s "$zsh_path" |
| 140 | ok "Shell changed (open a new WSL session for it to take effect)" |
| 141 | } |
| 142 | |
| 143 | install_oh_my_zsh() { |
| 144 | log "Installing Oh My Zsh and plugins..." |
| 145 | export RUNZSH=no |
| 146 | export CHSH=no |
| 147 | export KEEP_ZSHRC=yes |
| 148 | |
| 149 | if [[ ! -d "$HOME/.oh-my-zsh" ]]; then |
| 150 | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended |
| 151 | else |
| 152 | ok "Oh My Zsh already installed" |
| 153 | fi |
| 154 | |
| 155 | local custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" |
| 156 | local plugin_dir="$custom/plugins" |
| 157 | mkdir -p "$plugin_dir" |
| 158 | |
| 159 | [[ -d "$plugin_dir/zsh-autosuggestions" ]] || git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir/zsh-autosuggestions" |
| 160 | [[ -d "$plugin_dir/zsh-syntax-highlighting" ]] || git clone https://github.com/zsh-users/zsh-syntax-highlighting "$plugin_dir/zsh-syntax-highlighting" |
| 161 | |
| 162 | ok "Oh My Zsh plugins installed to $plugin_dir" |
| 163 | } |
| 164 | |
| 165 | install_starship() { |
| 166 | if command -v starship >/dev/null 2>&1 || [[ -f "$HOME/.local/bin/starship" ]]; then |
| 167 | ok "Starship already installed" |
| 168 | else |
| 169 | log "Installing Starship prompt..." |
| 170 | if command -v brew >/dev/null 2>&1; then |
| 171 | brew install starship |
| 172 | else |
| 173 | mkdir -p "$HOME/.local/bin" |
| 174 | curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin" |
| 175 | fi |
| 176 | ok "Starship installed" |
| 177 | fi |
| 178 | } |
| 179 | |
| 180 | download_configs() { |
| 181 | log "Downloading custom config files from OpenGist..." |
| 182 | local backup="$HOME/.config_backup_$(date +%Y%m%d_%H%M%S)" |
| 183 | mkdir -p "$backup" |
| 184 | |
| 185 | for f in "${CONFIG_FILES[@]}"; do |
| 186 | local remote_file="${f#.}" |
| 187 | [[ "$f" == ".config/starship.toml" ]] && remote_file="starship.toml" |
| 188 | local url="$GIST_RAW_BASE/$remote_file" |
| 189 | local target="$HOME/$f" |
| 190 | local tmp="${target}.tmp.$$" |
| 191 | |
| 192 | mkdir -p "$(dirname "$target")" |
| 193 | |
| 194 | if [[ -f "$target" ]]; then |
| 195 | cp "$target" "$backup/" |
| 196 | fi |
| 197 | |
| 198 | log "Fetching $remote_file -> $f ..." |
| 199 | if curl -fsSL "$url" -o "$tmp"; then |
| 200 | mv "$tmp" "$target" |
| 201 | else |
| 202 | rm -f "$tmp" |
| 203 | warn "Failed to download $remote_file" |
| 204 | fi |
| 205 | done |
| 206 | |
| 207 | ok "Configs downloaded (Backup at $backup)" |
| 208 | } |
| 209 | |
| 210 | configure_zshrc() { |
| 211 | local zshrc="$HOME/.zshrc" |
| 212 | log "Configuring .zshrc for Oh My Zsh and Starship..." |
| 213 | |
| 214 | touch "$zshrc" |
| 215 | cp "$zshrc" "$HOME/.zshrc.backup_$(date +%Y%m%d_%H%M%S)" |
| 216 | |
| 217 | cat > "$zshrc" <<'EOF' |
| 218 | # ============================================================================= |
| 219 | # 1. HELPER FUNCTIONS & PATH |
| 220 | # ============================================================================= |
| 221 | source_if_readable() { |
| 222 | local file="$1" |
| 223 | if [[ -f "$file" && -r "$file" ]]; then |
| 224 | source "$file" |
| 225 | fi |
| 226 | } |
| 227 | |
| 228 | export PATH="$HOME/.local/bin:$PATH" |
| 229 | |
| 230 | # ============================================================================= |
| 231 | # 2. OH MY ZSH FRAMEWORK |
| 232 | # ============================================================================= |
| 233 | export ZSH="${ZSH:-$HOME/.oh-my-zsh}" |
| 234 | ZSH_THEME="" |
| 235 | |
| 236 | plugins=( |
| 237 | git |
| 238 | zsh-autosuggestions |
| 239 | zsh-syntax-highlighting |
| 240 | ) |
| 241 | |
| 242 | source_if_readable "$ZSH/oh-my-zsh.sh" |
| 243 | |
| 244 | # ============================================================================= |
| 245 | # 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS |
| 246 | # ============================================================================= |
| 247 | source_if_readable "$HOME/.sourcerc" |
| 248 | source_if_readable "$HOME/.func" |
| 249 | source_if_readable "$HOME/.pathrc" |
| 250 | source_if_readable "$HOME/.alias" |
| 251 | |
| 252 | # ============================================================================= |
| 253 | # 4. STARSHIP PROMPT |
| 254 | # ============================================================================= |
| 255 | if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then |
| 256 | export STARSHIP_CONFIG="$HOME/.config/starship.toml" |
| 257 | fi |
| 258 | |
| 259 | if command -v starship >/dev/null 2>&1; then |
| 260 | eval "$(starship init zsh)" |
| 261 | elif [[ -x "$HOME/.local/bin/starship" ]]; then |
| 262 | eval "$("$HOME/.local/bin/starship" init zsh)" |
| 263 | fi |
| 264 | EOF |
| 265 | |
| 266 | ok ".zshrc configured for Oh My Zsh framework with Starship prompt" |
| 267 | } |
| 268 | |
| 269 | configure_git_identity() { |
| 270 | log "Configuring global Git identity..." |
| 271 | git config --global user.name "Vernon Wee Hong KOH" |
| 272 | git config --global user.email "261549452+weehong@users.noreply.github.com" |
| 273 | ok "Global Git identity configured" |
| 274 | } |
| 275 | |
| 276 | switch_shell() { |
| 277 | log "Starting Zsh session..." |
| 278 | echo -e "${YELLOW}Type 'exit' to return to this installer menu.${NC}" |
| 279 | echo "----------------------------------------" |
| 280 | zsh -l |
| 281 | echo "----------------------------------------" |
| 282 | ok "Returned from Zsh session" |
| 283 | } |
| 284 | |
| 285 | # ============================= |
| 286 | # INTERACTIVE MENU |
| 287 | # ============================= |
| 288 | show_menu() { |
| 289 | echo "===========================================" |
| 290 | echo "WSL Minimal Zsh Installer - Choose what to do" |
| 291 | echo "===========================================" |
| 292 | echo " 0) Run ALL steps (1-11)" |
| 293 | echo " 1) Update system packages" |
| 294 | echo " 2) Install core packages (zsh, git, vim, etc.)" |
| 295 | echo " 3) Set Timezone (best effort)" |
| 296 | echo " 4) Install Homebrew" |
| 297 | echo " 5) Configure shell (chsh - sets default shell)" |
| 298 | echo " 6) Install Oh My Zsh + plugins" |
| 299 | echo " 7) Install Starship prompt" |
| 300 | echo " 8) Download custom configs (from OpenGist)" |
| 301 | echo " 9) Configure ~/.zshrc (Oh My Zsh + Starship)" |
| 302 | echo "10) Configure global Git identity" |
| 303 | echo "11) Switch to Zsh (Temporary Sub-shell)" |
| 304 | echo "12) Quit" |
| 305 | echo "===========================================" |
| 306 | } |
| 307 | |
| 308 | run_choices() { |
| 309 | local input |
| 310 | read -p "Select: " input |
| 311 | input="${input//,/ }" |
| 312 | |
| 313 | local -a to_run=() |
| 314 | local -a to_exclude=() |
| 315 | |
| 316 | for item in $input; do |
| 317 | if [[ "$item" == !* ]]; then |
| 318 | to_exclude+=("${item:1}") |
| 319 | elif [[ "$item" == "0" ]]; then |
| 320 | to_run+=(1 2 3 4 5 6 7 8 9 10 11) |
| 321 | else |
| 322 | to_run+=("$item") |
| 323 | fi |
| 324 | done |
| 325 | |
| 326 | for choice in "${to_run[@]}"; do |
| 327 | local skip=false |
| 328 | |
| 329 | for ex in "${to_exclude[@]}"; do |
| 330 | if [[ "$choice" == "$ex" ]]; then |
| 331 | skip=true |
| 332 | break |
| 333 | fi |
| 334 | done |
| 335 | |
| 336 | $skip && continue |
| 337 | |
| 338 | case "$choice" in |
| 339 | 1) update_system ;; |
| 340 | 2) install_packages ;; |
| 341 | 3) set_timezone ;; |
| 342 | 4) install_homebrew ;; |
| 343 | 5) configure_shell ;; |
| 344 | 6) install_oh_my_zsh ;; |
| 345 | 7) install_starship ;; |
| 346 | 8) download_configs ;; |
| 347 | 9) configure_zshrc ;; |
| 348 | 10) configure_git_identity ;; |
| 349 | 11) switch_shell ;; |
| 350 | 12) log "Exiting..."; exit 0 ;; |
| 351 | *) warn "Skipping invalid option: $choice" ;; |
| 352 | esac |
| 353 | echo |
| 354 | done |
| 355 | } |
| 356 | |
| 357 | # ============================= |
| 358 | # MAIN |
| 359 | # ============================= |
| 360 | main() { |
| 361 | check_requirements |
| 362 | while true; do |
| 363 | show_menu |
| 364 | run_choices |
| 365 | read -p "Do you want to run more options? (y/n): " again |
| 366 | [[ "$again" =~ ^[Yy]$ ]] || break |
| 367 | done |
| 368 | ok "WSL Zsh installation/configuration complete!" |
| 369 | } |
| 370 | |
| 371 | main "$@" |
| 372 |
| 1 | # ============================================================================= |
| 2 | # 1. HELPER FUNCTIONS & PATH |
| 3 | # ============================================================================= |
| 4 | source_if_readable() { |
| 5 | local file="$1" |
| 6 | if [[ -f "$file" && -r "$file" ]]; then |
| 7 | source "$file" |
| 8 | fi |
| 9 | } |
| 10 | |
| 11 | # Ensure local bin is in PATH early (catches manual Starship installations) |
| 12 | export PATH="$HOME/.local/bin:$PATH" |
| 13 | |
| 14 | # ============================================================================= |
| 15 | # 2. OH MY ZSH FRAMEWORK |
| 16 | # ============================================================================= |
| 17 | export ZSH="${ZSH:-$HOME/.oh-my-zsh}" |
| 18 | ZSH_THEME="" |
| 19 | |
| 20 | plugins=( |
| 21 | git |
| 22 | zsh-autosuggestions |
| 23 | zsh-syntax-highlighting |
| 24 | ) |
| 25 | |
| 26 | source_if_readable "$ZSH/oh-my-zsh.sh" |
| 27 | |
| 28 | # ============================================================================= |
| 29 | # 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS |
| 30 | # ============================================================================= |
| 31 | # Load external initializers (SDKMAN, NVM, etc.) |
| 32 | source_if_readable "$HOME/.sourcerc" |
| 33 | |
| 34 | # Custom Functions (must load before Path Management) |
| 35 | source_if_readable "$HOME/.func" |
| 36 | |
| 37 | # Path Management (Relies on append_path from .func) |
| 38 | source_if_readable "$HOME/.pathrc" |
| 39 | |
| 40 | # Aliases (Loaded late so they override framework/system defaults) |
| 41 | source_if_readable "$HOME/.alias" |
| 42 | |
| 43 | # ============================================================================= |
| 44 | # 4. STARSHIP PROMPT |
| 45 | # ============================================================================= |
| 46 | if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then |
| 47 | export STARSHIP_CONFIG="$HOME/.config/starship.toml" |
| 48 | fi |
| 49 | |
| 50 | # Starship owns the prompt. It must initialize after Oh My Zsh. |
| 51 | if command -v starship >/dev/null 2>&1; then |
| 52 | eval "$(starship init zsh)" |
| 53 | elif [[ -x "$HOME/.local/bin/starship" ]]; then |
| 54 | eval "$("$HOME/.local/bin/starship" init zsh)" |
| 55 | fi |
| 56 |