From fd8b8c989e01393a5ad0543a2c565bdec4971791 Mon Sep 17 00:00:00 2001 From: Quan Deng Date: Mon, 12 Jan 2026 18:23:07 +0000 Subject: [PATCH 1/2] fix(installer): respect BIN_DIR for PATH and tmux scripts --- config/tmux-ccb.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/tmux-ccb.conf b/config/tmux-ccb.conf index dfaff19..b06ceb7 100644 --- a/config/tmux-ccb.conf +++ b/config/tmux-ccb.conf @@ -168,6 +168,10 @@ bind M set -g mouse off \; display "Mouse OFF" bind r source-file ~/.tmux.conf \; display "Config reloaded!" +# Manually toggle CCB theme for current session (optional). +bind-key C run-shell "#{@ccb_bin_dir}/ccb-tmux-on.sh" +bind-key V run-shell "#{@ccb_bin_dir}/ccb-tmux-off.sh" + # ----------------------------------------------------------------------------- # Session Management # ----------------------------------------------------------------------------- From 4d4995a27a9369144922bd871ffdc7472206dd4d Mon Sep 17 00:00:00 2001 From: Quan Deng Date: Fri, 30 Jan 2026 17:29:41 +0000 Subject: [PATCH 2/2] fix(runtime): honor CODEX_BIN_DIR for tmux + hooks --- README.md | 8 +++--- README_zh.md | 8 +++--- bin/ccb-completion-hook | 36 +++++++++++++++++++---- ccb | 64 +++++++++++++++++++++++++++++++++++++---- config/tmux-ccb.conf | 4 +-- lib/completion_hook.py | 19 ++++++++++-- 6 files changed, 117 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 942d027..69e4724 100644 --- a/README.md +++ b/README.md @@ -453,13 +453,13 @@ Check distro name with `wsl -l -v` in PowerShell. If `ccb`, `cask`, `cping` commands are not found after running `./install.sh install`: -**Cause:** The install directory (`~/.local/bin`) is not in your PATH. +**Cause:** The install directory (`$CODEX_BIN_DIR` or default `~/.local/bin`) is not in your PATH. **Solution:** ```bash # 1. Check if install directory exists -ls -la ~/.local/bin/ +ls -la "${CODEX_BIN_DIR:-$HOME/.local/bin}/" # 2. Check if PATH includes the directory echo $PATH | tr ':' '\n' | grep local @@ -468,7 +468,7 @@ echo $PATH | tr ':' '\n' | grep local cat ~/.zshrc | grep local # 4. If not configured, add manually -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc +echo 'export PATH="${CODEX_BIN_DIR:-$HOME/.local/bin}:$PATH"' >> ~/.zshrc # 5. Reload config source ~/.zshrc @@ -482,7 +482,7 @@ If WezTerm cannot find ccb commands but regular Terminal can: - Add PATH to `~/.zprofile` as well: ```bash -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zprofile +echo 'export PATH="${CODEX_BIN_DIR:-$HOME/.local/bin}:$PATH"' >> ~/.zprofile ``` Then restart WezTerm completely (Cmd+Q, reopen). diff --git a/README_zh.md b/README_zh.md index 631b1d2..11378f5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -474,13 +474,13 @@ cping 如果运行 `./install.sh install` 后找不到 `ccb`、`cask`、`cping` 等命令: -**原因:** 安装目录 (`~/.local/bin`) 不在 PATH 中。 +**原因:** 安装目录(`$CODEX_BIN_DIR`,或默认 `~/.local/bin`)不在 PATH 中。 **解决方法:** ```bash # 1. 检查安装目录是否存在 -ls -la ~/.local/bin/ +ls -la "${CODEX_BIN_DIR:-$HOME/.local/bin}/" # 2. 检查 PATH 是否包含该目录 echo $PATH | tr ':' '\n' | grep local @@ -489,7 +489,7 @@ echo $PATH | tr ':' '\n' | grep local cat ~/.zshrc | grep local # 4. 如果没有配置,手动添加 -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc +echo 'export PATH="${CODEX_BIN_DIR:-$HOME/.local/bin}:$PATH"' >> ~/.zshrc # 5. 重新加载配置 source ~/.zshrc @@ -503,7 +503,7 @@ source ~/.zshrc - 同时添加 PATH 到 `~/.zprofile`: ```bash -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zprofile +echo 'export PATH="${CODEX_BIN_DIR:-$HOME/.local/bin}:$PATH"' >> ~/.zprofile ``` 然后完全重启 WezTerm(Cmd+Q 退出后重新打开)。 diff --git a/bin/ccb-completion-hook b/bin/ccb-completion-hook index 5b17d4b..f18c711 100755 --- a/bin/ccb-completion-hook +++ b/bin/ccb-completion-hook @@ -24,6 +24,7 @@ import argparse import json import os import subprocess +import shutil import sys from pathlib import Path @@ -186,11 +187,28 @@ def send_via_tmux(pane_id: str, message: str) -> bool: def find_ask_command() -> str | None: """Find the ask command in common locations.""" + found = shutil.which("ask") + if found: + return found + ask_paths = [ + # Prefer the sibling command in the same installed bin folder. Path(__file__).resolve().parent / "ask", + ] + + bin_dir = (os.environ.get("CODEX_BIN_DIR") or "").strip() + if bin_dir: + ask_paths.append(Path(bin_dir).expanduser() / "ask") + + install_prefix = (os.environ.get("CODEX_INSTALL_PREFIX") or "").strip() + if install_prefix: + ask_paths.append(Path(install_prefix).expanduser() / "bin" / "ask") + + # Legacy/default locations + ask_paths.extend([ Path.home() / ".local" / "share" / "codex-dual" / "bin" / "ask", Path.home() / ".local" / "bin" / "ask", - ] + ]) # On Windows, also check LOCALAPPDATA if os.name == "nt": localappdata = os.environ.get("LOCALAPPDATA", "") @@ -365,7 +383,10 @@ Result: {reply_content} session_filename = session_files.get(caller, ".claude-session") # Search for session file in multiple locations (order matters - most specific first) - work_dir = os.environ.get("CCB_WORK_DIR", "") + work_dir = os.environ.get("CCB_WORK_DIR", "").strip() + if not work_dir: + work_dir = os.getcwd() + search_paths = [] # 1. Request's work_dir (most specific) @@ -374,13 +395,18 @@ Result: {reply_content} # 2. Current working directory (fallback) cwd = os.getcwd() - if cwd != work_dir: + if cwd and cwd != work_dir: search_paths.append(Path(cwd) / ".ccb_config" / session_filename) - # 3. User's home-based locations + # 3. Explicit install prefix (optional) + install_prefix = (os.environ.get("CODEX_INSTALL_PREFIX") or "").strip() + if install_prefix: + search_paths.append(Path(install_prefix).expanduser() / ".ccb_config" / session_filename) + + # 4. Legacy/default install location search_paths.append(Path.home() / ".local" / "share" / "codex-dual" / ".ccb_config" / session_filename) - # 4. On Windows, also check LOCALAPPDATA + # 5. On Windows, also check LOCALAPPDATA if os.name == "nt": localappdata = os.environ.get("LOCALAPPDATA", "") if localappdata: diff --git a/ccb b/ccb index b5b7afa..b78d070 100755 --- a/ccb +++ b/ccb @@ -53,6 +53,55 @@ _MNT_DRIVE_RE = re.compile(r"^/mnt/([A-Za-z])/(.*)$") _MSYS_DRIVE_RE = re.compile(r"^/([A-Za-z])/(.*)$") +def _get_bin_dir() -> Path: + """Best-effort resolve the bin dir where CCB helper scripts live. + + Priority: + 1) CODEX_BIN_DIR / CCB_BIN_DIR env + 2) directory of the invoked executable on PATH (shutil.which) + 3) default ~/.local/bin + """ + env_bin = (os.environ.get("CODEX_BIN_DIR") or os.environ.get("CCB_BIN_DIR") or "").strip() + if env_bin: + return Path(env_bin).expanduser() + + argv0 = (sys.argv[0] or "").strip() + candidates: list[Path] = [] + if argv0: + # If invoked via an explicit path, keep its directory (do NOT resolve symlinks). + if ("/" in argv0) or ("\\" in argv0): + candidates.append(Path(argv0).expanduser()) + found = shutil.which(argv0) + if found: + candidates.append(Path(found)) + + found_ccb = shutil.which("ccb") + if found_ccb: + candidates.append(Path(found_ccb)) + + for p in candidates: + try: + if p.exists(): + return p.parent + except Exception: + continue + + return Path.home() / ".local" / "bin" + + +def _find_helper_script(name: str) -> Path | None: + """Locate an installed helper script by name.""" + found = shutil.which(name) + if found: + return Path(found) + + for base in (_get_bin_dir(), script_dir / "config"): + p = base / name + if p.exists(): + return p + return None + + def _looks_like_windows_path(value: str) -> bool: s = value.strip() if not s: @@ -901,14 +950,15 @@ class AILauncher: Enable/disable CCB tmux UI theming for the *current tmux session*. This is session-scoped and reversible (saves/restores user options) via helper scripts - installed to `~/.local/bin/`. + installed to BIN_DIR (see env `CODEX_BIN_DIR`). """ if self.terminal_type != "tmux": return if not os.environ.get("TMUX"): return - script = Path.home() / ".local" / "bin" / ("ccb-tmux-on.sh" if active else "ccb-tmux-off.sh") - if not script.exists(): + script_name = "ccb-tmux-on.sh" if active else "ccb-tmux-off.sh" + script = _find_helper_script(script_name) + if not script: return try: debug = os.environ.get("CCB_DEBUG") in ("1", "true", "yes") @@ -3847,9 +3897,13 @@ def _detect_cca() -> tuple[str | None, str | None]: install_dir = _infer_install_dir_from_exe(exe) return str(exe.resolve()), str(install_dir) candidates = [ - Path.home() / ".local/share/claude_code_autoflow", - Path.home() / ".local/bin/cca", + Path.home() / ".local" / "share" / "claude_code_autoflow", ] + # If user configured a custom bin dir for tools, also consider it for legacy fallbacks. + env_bin_dir = (os.environ.get("CODEX_BIN_DIR") or os.environ.get("CCB_BIN_DIR") or "").strip() + if env_bin_dir: + candidates.append(Path(env_bin_dir).expanduser() / "cca") + candidates.append(Path.home() / ".local" / "bin" / "cca") # Windows 特定路径 if platform.system() == "Windows": localappdata = os.environ.get("LOCALAPPDATA", "") diff --git a/config/tmux-ccb.conf b/config/tmux-ccb.conf index b06ceb7..b222e7b 100644 --- a/config/tmux-ccb.conf +++ b/config/tmux-ccb.conf @@ -195,9 +195,9 @@ set -g visual-activity off # NOTE: # CCB intentionally does not set any persistent statusbar/theme options here. # The CCB theme is applied per-session only while CCB is active via: -# - `~/.local/bin/ccb-tmux-on.sh` +# - `$CODEX_BIN_DIR/ccb-tmux-on.sh` (or default `~/.local/bin/ccb-tmux-on.sh`) # and restored on exit via: -# - `~/.local/bin/ccb-tmux-off.sh` +# - `$CODEX_BIN_DIR/ccb-tmux-off.sh` (or default `~/.local/bin/ccb-tmux-off.sh`) # # This avoids clobbering your existing tmux theme when CCB is not running. diff --git a/lib/completion_hook.py b/lib/completion_hook.py index 11fc997..72c7523 100644 --- a/lib/completion_hook.py +++ b/lib/completion_hook.py @@ -9,6 +9,7 @@ import os import subprocess +import shutil import sys import threading from pathlib import Path @@ -40,11 +41,25 @@ def _run_hook_async( def _run(): try: # Find ccb-completion-hook script (Python script only, not .cmd wrapper) - script_paths = [ + script_paths: list[Path] = [] + + found = shutil.which("ccb-completion-hook") + if found: + script_paths.append(Path(found)) + + bin_dir = (os.environ.get("CODEX_BIN_DIR") or "").strip() + if bin_dir: + script_paths.append(Path(bin_dir).expanduser() / "ccb-completion-hook") + + install_prefix = (os.environ.get("CODEX_INSTALL_PREFIX") or "").strip() + if install_prefix: + script_paths.append(Path(install_prefix).expanduser() / "bin" / "ccb-completion-hook") + + script_paths.extend([ Path(__file__).parent.parent / "bin" / "ccb-completion-hook", Path.home() / ".local" / "bin" / "ccb-completion-hook", Path("/usr/local/bin/ccb-completion-hook"), - ] + ]) # On Windows, check installed location (Python script, not .cmd) if os.name == "nt": localappdata = os.environ.get("LOCALAPPDATA", "")