diff --git a/README.md b/README.md index 8ed8442..38b3824 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This config folder includes configurations for various development tools and app ### Quick Setup Run the deployment script to install all tools and configure symlinks: ```bash -./deploy.sh +bin/upgrade-all ``` The script is idempotent - you can run it multiple times safely. It will: diff --git a/README_cn.md b/README_cn.md index 9329131..6bc2bdd 100644 --- a/README_cn.md +++ b/README_cn.md @@ -11,7 +11,7 @@ ### 快速设置 运行部署脚本来安装所有工具并配置符号链接: ```bash -./deploy.sh +bin/upgrade-all ``` 脚本具有幂等性 - 你可以多次安全地运行它。它将: @@ -143,4 +143,4 @@ yay -S wqy-bitmapfont wqy-microhei wqy-microhei-lite wqy-zenhei adobe-source-han ### Arch 软件包 查看 [my-packages.txt](https://github.com/theniceboy/.config/blob/master/my-packages.txt) 获取完整软件包列表。 - \ No newline at end of file + diff --git a/bin/upgrade-all b/bin/upgrade-all index 606d569..afbc6b7 100755 --- a/bin/upgrade-all +++ b/bin/upgrade-all @@ -1,8 +1,10 @@ #!/usr/bin/env python3 import json +import os import shutil import subprocess +import tempfile import threading import time import sys @@ -203,6 +205,137 @@ NPM_PACKAGE_LOCKS = { # "@openai/codex": "0.40.0", } +def ensure_homebrew(): + if shutil.which("brew") is not None: + return True, "(found)", "" + + candidate_paths = [ + "/opt/homebrew/bin/brew", + "/usr/local/bin/brew", + "/home/linuxbrew/.linuxbrew/bin/brew", + os.path.expanduser("~/.linuxbrew/bin/brew"), + ] + + def configure_env(brew_path): + env_result = subprocess.run( + [brew_path, "shellenv"], capture_output=True, text=True + ) + for line in env_result.stdout.splitlines(): + if not line.startswith("export "): + continue + key_val = line[len("export "):] + if "=" not in key_val: + continue + key, val = key_val.split("=", 1) + val = val.strip().strip('"') + if "${PATH" in val or "$PATH" in val: + current_path = os.environ.get("PATH", "") + val = val.replace("${PATH+:$PATH}", f":{current_path}" if current_path else "") + val = val.replace("$PATH", current_path) + os.environ[key] = val + if shutil.which("brew") is None: + os.environ["PATH"] = f"{os.path.dirname(brew_path)}:{os.environ.get('PATH','')}" + + for path in candidate_paths: + if os.path.exists(path): + configure_env(path) + return True, "(found)", "" + + install_output_parts = [] + script_url = "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + try: + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp_path = tmp.name + fetch = subprocess.run( + ["curl", "-fsSL", script_url, "-o", tmp_path], + capture_output=True, + text=True, + ) + install_output_parts.append(fetch.stdout or "") + install_output_parts.append(fetch.stderr or "") + if fetch.returncode != 0: + os.unlink(tmp_path) + return False, "", "".join(install_output_parts) + + env = os.environ.copy() + env["NONINTERACTIVE"] = "1" + run_install = subprocess.run( + ["/bin/bash", tmp_path], + capture_output=True, + text=True, + env=env, + ) + install_output_parts.append(run_install.stdout or "") + install_output_parts.append(run_install.stderr or "") + os.unlink(tmp_path) + + if run_install.returncode != 0: + return False, "", "".join(install_output_parts) + except Exception as exc: + return False, "", f"{''.join(install_output_parts)}\n{exc}" + + for path in candidate_paths: + if os.path.exists(path): + configure_env(path) + return True, "(installed)", "".join(install_output_parts) + + return False, "", "".join(install_output_parts) + +def ensure_git_available(): + if shutil.which("git") is not None: + return True, "(found)", "" + + result = subprocess.run( + ["brew", "install", "git"], + capture_output=True, + text=True, + ) + output = f"{result.stdout or ''}{result.stderr or ''}" + if result.returncode == 0: + return True, "(installed)", output + return False, "", output + +def ensure_github_known_host(): + ssh_dir = os.path.expanduser("~/.ssh") + known_hosts = os.path.join(ssh_dir, "known_hosts") + os.makedirs(ssh_dir, exist_ok=True) + + check = subprocess.run( + ["ssh-keygen", "-F", "github.com"], + capture_output=True, + text=True, + ) + if check.returncode == 0 and check.stdout: + return True + + scan = subprocess.run( + ["ssh-keyscan", "-H", "github.com"], + capture_output=True, + text=True, + ) + if scan.returncode != 0: + return False + + with open(known_hosts, "a", encoding="utf-8") as f: + f.write(scan.stdout) + return True + +def check_github_ssh(): + if not ensure_github_known_host(): + return False, "", "failed to add github.com host key" + + cmd = ["ssh", "-o", "BatchMode=yes", "-T", "git@github.com"] + result = subprocess.run(cmd, capture_output=True, text=True) + output = f"{result.stdout or ''}{result.stderr or ''}" + ok_phrase = "successfully authenticated" + if result.returncode in (0, 1) and ok_phrase in output: + return True, "(auth ok)", output + if "Permission denied" in output: + return False, "", "GitHub SSH key rejected (Permission denied)" + if "Could not resolve hostname" in output: + return False, "", "Cannot reach github.com" + return False, "", output + def install_brew_packages(): """Install missing brew packages.""" print("📦 Checking brew packages...") @@ -216,10 +349,10 @@ def install_brew_packages(): print("❌ Failed to get brew package list") return False - packages = [ + base_packages = [ # System utilities - "htop", "dust", "ncdu", "fswatch", "pipx", "uv", "terminal-notifier", - # macOS GNU utilities + "htop", "dust", "ncdu", "fswatch", "pipx", "uv", + # macOS GNU utilities / common core tools "coreutils", "gnu-tar", "gnu-getopt", "gnu-sed", # Development tools "node", "go", "gcc", "gdb", "automake", "cmake", "jsdoc3", "oven-sh/bun/bun", @@ -236,8 +369,16 @@ def install_brew_packages(): # Terminal and shell "tmux", "neovim", "starship", "rainbarf", "neofetch", "onefetch", "tldr", "bmon", "loc", # Applications - "awscli", "azure-cli", "terraform" + "awscli", "azure-cli", "hashicorp/tap/terraform", ] + + mac_only = ["terminal-notifier"] + linux_skip = ["oven-sh/bun/bun"] + + system = os.uname().sysname + packages = base_packages + (mac_only if system == "Darwin" else []) + if system == "Linux": + packages = [p for p in packages if p not in linux_skip] missing_packages = [] installed_list = installed_packages.split('\n') @@ -252,14 +393,26 @@ def install_brew_packages(): if package not in installed_list: missing_packages.append(package) + failed = [] if missing_packages: print(f"Installing {len(missing_packages)} missing packages...") for package in missing_packages: try: + if package.startswith("hashicorp/tap/"): + subprocess.run( + ['brew', 'tap', 'hashicorp/tap'], + capture_output=True, + text=True, + check=True + ) subprocess.run(['brew', 'install', package], check=True) print(f"✅ Installed {package}") except subprocess.CalledProcessError: print(f"❌ Failed to install {package}") + failed.append(package) + if failed: + print(f"❌ Failed packages: {', '.join(failed)}") + return False return True else: print("✅ All packages already installed") @@ -485,20 +638,274 @@ def schedule_npm_updates(runner): runner.add_task("Node Apps Update", action=create_npm_update_action(packages, locks=NPM_PACKAGE_LOCKS)) +def interpret_action_result(result): + success = True + note = '' + output = '' + if isinstance(result, tuple): + if len(result) == 3: + success, note, output = result + elif len(result) == 2: + success, output = result + elif isinstance(result, dict): + success = bool(result.get('success', True)) + note = result.get('note', '') + output = result.get('output', '') + elif isinstance(result, bool): + success = result + elif result is not None: + output = str(result) + return bool(success), note, output + +def ensure_symlink(target, link_name): + target = os.path.expanduser(target) + link_name = os.path.expanduser(link_name) + + if os.path.islink(link_name): + if os.readlink(link_name) == target: + return True, "(already linked)", "" + os.remove(link_name) + elif os.path.exists(link_name): + backup = f"{link_name}.backup" + while os.path.exists(backup): + backup = f"{backup}.{int(time.time())}" + os.rename(link_name, backup) + + os.symlink(target, link_name) + return True, f"(linked to {target})", "" + +def create_symlink_action(target, link_name): + def action(): + return ensure_symlink(target, link_name) + return action + +def ensure_repo(path, slug, must_exist=False): + path = os.path.expanduser(path) + repo_url = f"git@github.com:{slug}.git" + def normalize(url): + return url.rstrip("/").removesuffix(".git") + + if not os.path.exists(path): + if must_exist: + return False, "", f"{path} missing" + os.makedirs(os.path.dirname(path), exist_ok=True) + clone = subprocess.run( + ["git", "clone", repo_url, path], + capture_output=True, + text=True, + ) + output = f"{clone.stdout or ''}{clone.stderr or ''}" + if clone.returncode != 0: + return False, "", output + return True, "(cloned)", output + + git_dir = os.path.join(path, ".git") + if not os.path.isdir(git_dir): + return False, "", f"{path} exists but is not a git repo" + + remote = subprocess.run( + ["git", "-C", path, "config", "--get", "remote.origin.url"], + capture_output=True, + text=True, + ) + remote_url = (remote.stdout or "").strip() + if remote.returncode != 0: + return False, "", f"cannot read remote for {path}" + if normalize(remote_url) != normalize(repo_url): + return False, "", f"remote mismatch: {remote_url}" + + fetch = subprocess.run( + ["git", "-C", path, "fetch", "--all", "--prune"], + capture_output=True, + text=True, + ) + fetch_output = f"{fetch.stdout or ''}{fetch.stderr or ''}" + if fetch.returncode != 0: + return False, "", fetch_output + + pull = subprocess.run( + ["git", "-C", path, "pull", "--ff-only"], + capture_output=True, + text=True, + ) + pull_output = f"{pull.stdout or ''}{pull.stderr or ''}" + if pull.returncode != 0: + return False, "", f"{fetch_output}{pull_output}" + + return True, "(updated)", f"{fetch_output}{pull_output}" + +def create_repo_action(path, slug, must_exist=False): + def action(): + return ensure_repo(path, slug, must_exist=must_exist) + return action + +def is_writable(path): + path = os.path.expanduser(path) + try: + test_file = os.path.join(path, ".writetest") + with open(test_file, "w", encoding="utf-8") as f: + f.write("ok") + os.remove(test_file) + return True + except OSError: + return False + +def ensure_paths_writable(paths): + for p in paths: + if not is_writable(p): + return False, "", f"path not writable: {os.path.expanduser(p)}" + return True, "(writable)", "" + +def ensure_system_build_tools(): + if os.uname().sysname != "Linux": + return True, "(not needed on macOS)", "" + update = subprocess.run( + ["sudo", "apt-get", "update", "-y"], + capture_output=True, + text=True, + ) + output_parts = [update.stdout or "", update.stderr or ""] + if update.returncode != 0: + return False, "", "".join(output_parts) + + install = subprocess.run( + ["sudo", "apt-get", "install", "-y", "build-essential"], + capture_output=True, + text=True, + ) + output_parts.extend([install.stdout or "", install.stderr or ""]) + if install.returncode != 0: + return False, "", "".join(output_parts) + return True, "(build-essential ok)", "".join(output_parts) + +def create_zshrc_action(): + def action(): + zshrc_path = os.path.expanduser("~/.zshrc") + source_line = "source ~/.config/zsh/zshrc" + + if not os.path.exists(zshrc_path): + with open(zshrc_path, "w", encoding="utf-8") as f: + f.write(f"{source_line}\n") + return True, "(created ~/.zshrc)", "" + + with open(zshrc_path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + if any(source_line in line for line in lines): + return True, "(already sources config)", "" + + with open(zshrc_path, "a", encoding="utf-8") as f: + if lines and lines[-1].strip() != "": + f.write("\n") + f.write(f"{source_line}\n") + return True, "(added zsh source line)", "" + + return action + +def create_command_action(command): + def action(): + result = subprocess.run( + command, + capture_output=True, + text=True, + ) + output = f"{result.stdout or ''}{result.stderr or ''}" + return result.returncode == 0, "", output + return action + +def create_agent_tracker_service_action(): + def action(): + env = os.environ.copy() + env["HOMEBREW_NO_AUTO_UPDATE"] = "1" + + service = subprocess.run( + ["bash", os.path.expanduser("~/.config/agent-tracker/scripts/install_brew_service.sh")], + capture_output=True, + text=True, + env=env, + ) + output = f"{service.stdout or ''}{service.stderr or ''}" + if service.returncode != 0: + return False, "", output + return True, "(service installed)", output + + return action + +def create_agent_tracker_build_action(): + def action(): + base = os.path.expanduser("~/.config/agent-tracker") + if not os.path.isdir(base): + return False, "", f"{base} missing" + + build = subprocess.run( + ["bash", "-lc", "cd ~/.config/agent-tracker && ./install.sh"], + capture_output=True, + text=True, + ) + output = f"{build.stdout or ''}{build.stderr or ''}" + if build.returncode != 0: + return False, "", output + + binary = os.path.join(base, "bin", "tracker-server") + if not os.path.isfile(binary): + return False, "", f"tracker-server still missing after build ({binary})" + return True, "(built)", output + + return action + +def run_inline(name, action): + success, note, output = interpret_action_result(action()) + prefix = f"{Colors.GREEN}[OK]{Colors.RESET}" if success else f"{Colors.RED}[FAIL]{Colors.RESET}" + note_part = f" {note}" if note else "" + print(f"{prefix} {name}{note_part}") + if not success and output: + lines = output.strip().splitlines() + for line in lines[:10]: + print(f" {line}") + if len(lines) > 10: + print(f" ... ({len(lines) - 10} more lines)") + return success + def main(): runner = ParallelTaskRunner() + pre_steps = [ + ("Homebrew Available", ensure_homebrew), + ("Git Available", ensure_git_available), + ("GitHub SSH Auth", check_github_ssh), + ("Paths Writable", lambda: ensure_paths_writable(["~", "~/.config", "~/.config/bin"])), + ("System Build Tools", ensure_system_build_tools), + ("Repo ~/.config", create_repo_action("~/.config", "theniceboy/.config", must_exist=True)), + ("Repo ~/.config/nvim", create_repo_action("~/.config/nvim", "theniceboy/nvim")), + ("Repo ~/.sconfig", create_repo_action("~/.sconfig", "theniceboy/.sconfig")), + ("Repo ~/Github/mac-ctrl", create_repo_action("~/Github/mac-ctrl", "theniceboy/mac-ctrl")), + ] + + for name, action in pre_steps: + if not run_inline(name, action): + sys.exit(1) + # Install missing brew packages first if not install_brew_packages(): print("❌ Brew package installation failed") sys.exit(1) + env_steps = [ + ("Zsh Config Ensure", create_zshrc_action()), + ("Symlink Tmux Config", create_symlink_action("~/.config/.tmux.conf", "~/.tmux.conf")), + ("Symlink Claude Config", create_symlink_action("~/.config/claude", "~/.claude")), + ("Symlink Codex Config", create_symlink_action("~/.config/codex", "~/.codex")), + ("Symlink Ripgrep Config", create_symlink_action("~/.config/rg/ripgreprc", "~/.ripgreprc")), + ("Agent Tracker Build", create_agent_tracker_build_action()), + ("Agent Tracker Brew Service", create_agent_tracker_service_action()), + ] + + for name, action in env_steps: + if not run_inline(name, action): + sys.exit(1) + schedule_npm_updates(runner) runner.add_task("Homebrew Update", action=create_brew_update_action()) - runner.add_task("Config Git Pull", "cd ~/.config && git pull") - runner.add_task("Neovim Config Git Pull", "cd ~/.config/nvim && git pull") - runner.add_task("SConfig Git Pull", "cd ~/.sconfig && git pull") - runner.add_task( "Agent Tracker Build", "cd ~/.config/agent-tracker && ./install.sh") + runner.add_task("Agent Tracker Build", "cd ~/.config/agent-tracker && ./install.sh") success = runner.run_all() sys.exit(0 if success else 1) diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 5a4b21b..0000000 --- a/deploy.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "🚀 Starting deployment script..." - -# Check if brew is installed or try to find it -if ! command -v brew &> /dev/null; then - echo "⚠️ Homebrew not in PATH, checking common locations..." - - # Try common Homebrew paths for macOS - if [[ "$OSTYPE" == "darwin"* ]]; then - for brew_path in "/opt/homebrew/bin/brew" "/usr/local/bin/brew"; do - if [[ -x "$brew_path" ]]; then - echo "🔧 Found Homebrew at $brew_path, setting up environment..." - eval "$($brew_path shellenv)" - break - fi - done - fi - - # Check again after PATH update - if ! command -v brew &> /dev/null; then - echo "❌ Homebrew not found" - echo "📦 Please install Homebrew first by running:" - echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" - echo "After installation, make sure to run the commands it suggests to add brew to your PATH." - echo "Then run this script again." - exit 1 - fi -fi - -echo "✅ Homebrew found" - -# Run upgrade-all to install/update packages -echo "📦 Running upgrade-all to install/update packages..." -if command -v upgrade-all &> /dev/null; then - upgrade-all -elif [ -x "$HOME/.config/bin/upgrade-all" ]; then - python3 "$HOME/.config/bin/upgrade-all" -else - echo "❌ upgrade-all script not found" - echo " Expected at: $HOME/.config/bin/upgrade-all" - exit 1 -fi - -# Ensure zsh configuration is sourced -echo "🔗 Setting up zsh configuration..." -if [ ! -f "$HOME/.zshrc" ]; then - echo "source ~/.config/zsh/zshrc" > "$HOME/.zshrc" - echo "✅ Created ~/.zshrc with config source" -elif ! grep -q "source ~/.config/zsh/zshrc" "$HOME/.zshrc"; then - echo "source ~/.config/zsh/zshrc" >> "$HOME/.zshrc" - echo "✅ Added config source to ~/.zshrc" -else - echo "✅ Zsh config source already exists in ~/.zshrc" -fi - -# Function to create symlinks -create_symlink() { - local target="$1" - local link_name="$2" - local display_name="$3" - - echo "🔗 Setting up $display_name symlink..." - - if [ -L "$link_name" ]; then - local current - current=$(readlink "$link_name") - if [ "$current" = "$target" ]; then - echo "✅ $display_name symlink already exists and is correct" - return 0 - fi - echo "⚠️ $link_name points to $current; updating to $target" - rm "$link_name" - elif [ -e "$link_name" ]; then - local backup="${link_name}.backup" - if [ -e "$backup" ]; then - backup="${backup}.$(date +%Y%m%d%H%M%S)" - fi - echo "⚠️ Backing up existing $link_name to $backup" - mv "$link_name" "$backup" - fi - - ln -s "$target" "$link_name" - echo "✅ Symlink ensured: $link_name -> $target" -} - -bash ~/.config/agent-tracker/scripts/install_brew_service.sh -# Create configuration symlinks -create_symlink "$HOME/.config/.tmux.conf" "$HOME/.tmux.conf" "Tmux" -create_symlink "$HOME/.config/claude" "$HOME/.claude" "Claude" -create_symlink "$HOME/.config/codex" "$HOME/.codex" "Codex" - -echo "🎉 Deployment complete!"