From 74ccd229d1acef0c89bdd5143c4bcca1ed00f1a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:56:09 +0000 Subject: [PATCH 1/4] Initial plan From 8f1f4c08308e983e059ec61db532148fb47cf8cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 08:02:02 +0000 Subject: [PATCH 2/4] Change default install location to ~/.local/bin (XDG compliance) Closes #XX Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- docs/install-pr.sh | 12 ++++++------ docs/install.sh | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/install-pr.sh b/docs/install-pr.sh index 245e47a..895f1a8 100644 --- a/docs/install-pr.sh +++ b/docs/install-pr.sh @@ -8,7 +8,7 @@ set -e # brew install gh && gh auth login REPO="davidfowl/tally" -INSTALL_DIR="${INSTALL_DIR:-$HOME/.tally/bin}" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" TMPDIR="${TMPDIR:-/tmp}" # Colors @@ -156,20 +156,20 @@ add_to_path() { else config_file="$HOME/.bashrc" fi - path_line='export PATH="$HOME/.tally/bin:$PATH"' + path_line='export PATH="$HOME/.local/bin:$PATH"' ;; zsh) config_file="${ZDOTDIR:-$HOME}/.zshrc" - path_line='export PATH="$HOME/.tally/bin:$PATH"' + path_line='export PATH="$HOME/.local/bin:$PATH"' ;; fish) config_file="${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" - path_line='fish_add_path $HOME/.tally/bin' + path_line='fish_add_path $HOME/.local/bin' ;; *) # Fallback to .profile for other POSIX shells config_file="$HOME/.profile" - path_line='export PATH="$HOME/.tally/bin:$PATH"' + path_line='export PATH="$HOME/.local/bin:$PATH"' ;; esac @@ -177,7 +177,7 @@ add_to_path() { mkdir -p "$(dirname "$config_file")" # Check if already added - if [[ -f "$config_file" ]] && grep -q "/.tally/bin" "$config_file" 2>/dev/null; then + if [[ -f "$config_file" ]] && grep -q "/.local/bin" "$config_file" 2>/dev/null; then return fi diff --git a/docs/install.sh b/docs/install.sh index b228ef0..3927fc7 100755 --- a/docs/install.sh +++ b/docs/install.sh @@ -6,7 +6,7 @@ set -e # Usage: curl -fsSL https://raw.githubusercontent.com/davidfowl/tally/main/install.sh | bash -s -- --prerelease REPO="davidfowl/tally" -INSTALL_DIR="${INSTALL_DIR:-$HOME/.tally/bin}" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" TMPDIR="${TMPDIR:-/tmp}" PRERELEASE=false @@ -150,20 +150,20 @@ add_to_path() { else config_file="$HOME/.bashrc" fi - path_line='export PATH="$HOME/.tally/bin:$PATH"' + path_line='export PATH="$HOME/.local/bin:$PATH"' ;; zsh) config_file="${ZDOTDIR:-$HOME}/.zshrc" - path_line='export PATH="$HOME/.tally/bin:$PATH"' + path_line='export PATH="$HOME/.local/bin:$PATH"' ;; fish) config_file="${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" - path_line='fish_add_path $HOME/.tally/bin' + path_line='fish_add_path $HOME/.local/bin' ;; *) # Fallback to .profile for other POSIX shells config_file="$HOME/.profile" - path_line='export PATH="$HOME/.tally/bin:$PATH"' + path_line='export PATH="$HOME/.local/bin:$PATH"' ;; esac @@ -171,7 +171,7 @@ add_to_path() { mkdir -p "$(dirname "$config_file")" # Check if already added - if [[ -f "$config_file" ]] && grep -q "/.tally/bin" "$config_file" 2>/dev/null; then + if [[ -f "$config_file" ]] && grep -q "/.local/bin" "$config_file" 2>/dev/null; then return fi From dd440510e38dbe1c37d4eee02ea7240f81a5eb3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 08:25:43 +0000 Subject: [PATCH 3/4] Fix test workflow to verify installation in ~/.local/bin Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .github/workflows/test-install.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index 55afd1f..f219b6d 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -34,8 +34,8 @@ jobs: - name: Verify installation run: | - ~/.tally/bin/tally version - ~/.tally/bin/tally --help + ~/.local/bin/tally version + ~/.local/bin/tally --help test-windows: runs-on: windows-latest From 4e94f387f96c5d9d6e91bf7ea6e1d636a9013935 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:53:28 +0000 Subject: [PATCH 4/4] Add migration logic for old ~/.tally/bin installations - Install scripts now detect and migrate from old location - Clean up old PATH entries from shell config files - tally update command migrates to ~/.local/bin automatically - Provides user feedback during migration process Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- docs/install-pr.sh | 48 +++++++++++++++++++++++++++++++++++++ docs/install.sh | 48 +++++++++++++++++++++++++++++++++++++ src/tally/_version.py | 56 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 147 insertions(+), 5 deletions(-) diff --git a/docs/install-pr.sh b/docs/install-pr.sh index 895f1a8..4907e7e 100644 --- a/docs/install-pr.sh +++ b/docs/install-pr.sh @@ -82,6 +82,51 @@ Check https://github.com/${REPO}/pull/${pr_number}/checks" echo "$run_id" } +# Migrate from old installation location +migrate_old_installation() { + local old_install_dir="$HOME/.tally/bin" + local old_binary="${old_install_dir}/tally" + + if [[ -f "$old_binary" ]]; then + info "Found existing installation at ${old_install_dir}" + info "Migrating to ${INSTALL_DIR}..." + + # Remove old binary + rm -f "$old_binary" + + # Remove old directory if empty + if [[ -d "$old_install_dir" ]] && [[ -z "$(ls -A "$old_install_dir")" ]]; then + rmdir "$old_install_dir" + fi + if [[ -d "$HOME/.tally" ]] && [[ -z "$(ls -A "$HOME/.tally")" ]]; then + rmdir "$HOME/.tally" + fi + + # Clean up old PATH entries from shell config files + local config_files=( + "$HOME/.bashrc" + "$HOME/.bash_profile" + "$HOME/.zshrc" + "${ZDOTDIR:-$HOME}/.zshrc" + "$HOME/.profile" + "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" + ) + + for config_file in "${config_files[@]}"; do + if [[ -f "$config_file" ]] && grep -q "/.tally/bin" "$config_file" 2>/dev/null; then + # Create backup + cp "$config_file" "${config_file}.bak" + # Remove lines containing .tally/bin + sed -i.tmp '/\.tally\/bin/d' "$config_file" 2>/dev/null || sed -i '' '/\.tally\/bin/d' "$config_file" 2>/dev/null + rm -f "${config_file}.tmp" + info "Cleaned up old PATH entry in $(basename "$config_file")" + fi + done + + info "Migration complete!" + fi +} + main() { local pr_number="$1" @@ -94,6 +139,9 @@ Example: $0 42" check_gh info "Installing tally from PR #${pr_number}..." + + # Migrate from old installation if it exists + migrate_old_installation OS=$(detect_os) ARCH=$(detect_arch) diff --git a/docs/install.sh b/docs/install.sh index 3927fc7..adbc8ab 100755 --- a/docs/install.sh +++ b/docs/install.sh @@ -64,12 +64,60 @@ get_release_version() { fi } +# Migrate from old installation location +migrate_old_installation() { + local old_install_dir="$HOME/.tally/bin" + local old_binary="${old_install_dir}/tally" + + if [[ -f "$old_binary" ]]; then + info "Found existing installation at ${old_install_dir}" + info "Migrating to ${INSTALL_DIR}..." + + # Remove old binary + rm -f "$old_binary" + + # Remove old directory if empty + if [[ -d "$old_install_dir" ]] && [[ -z "$(ls -A "$old_install_dir")" ]]; then + rmdir "$old_install_dir" + fi + if [[ -d "$HOME/.tally" ]] && [[ -z "$(ls -A "$HOME/.tally")" ]]; then + rmdir "$HOME/.tally" + fi + + # Clean up old PATH entries from shell config files + local config_files=( + "$HOME/.bashrc" + "$HOME/.bash_profile" + "$HOME/.zshrc" + "${ZDOTDIR:-$HOME}/.zshrc" + "$HOME/.profile" + "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" + ) + + for config_file in "${config_files[@]}"; do + if [[ -f "$config_file" ]] && grep -q "/.tally/bin" "$config_file" 2>/dev/null; then + # Create backup + cp "$config_file" "${config_file}.bak" + # Remove lines containing .tally/bin + sed -i.tmp '/\.tally\/bin/d' "$config_file" 2>/dev/null || sed -i '' '/\.tally\/bin/d' "$config_file" 2>/dev/null + rm -f "${config_file}.tmp" + info "Cleaned up old PATH entry in $(basename "$config_file")" + fi + done + + info "Migration complete!" + fi +} + main() { if [ "$PRERELEASE" = true ]; then info "Installing tally (development build)..." else info "Installing tally..." fi + + # Migrate from old installation if it exists + migrate_old_installation OS=$(detect_os) ARCH=$(detect_arch) diff --git a/src/tally/_version.py b/src/tally/_version.py index 638db97..2ca81a2 100644 --- a/src/tally/_version.py +++ b/src/tally/_version.py @@ -237,6 +237,7 @@ def get_install_path(): """Get the expected installation path for tally. Returns Path object for the install location based on platform. + For Unix systems, checks new location (~/.local/bin) first, then falls back to old location (~/.tally/bin). """ import platform as plat from pathlib import Path @@ -250,7 +251,14 @@ def get_install_path(): return Path(local_app_data) / 'tally' / 'tally.exe' else: # macOS, Linux home = Path.home() - return home / '.tally' / 'bin' / 'tally' + new_path = home / '.local' / 'bin' / 'tally' + old_path = home / '.tally' / 'bin' / 'tally' + + # If running from old location, return old path so update can work + # Otherwise return new location + if old_path.exists(): + return old_path + return new_path return None @@ -288,17 +296,29 @@ def perform_update(release_info: dict, force: bool = False) -> tuple[bool, str]: download_url = release_info['assets'][asset_name] # Determine install path - install_path = get_executable_path() or get_install_path() + current_path = get_executable_path() + install_path = current_path or get_install_path() if not install_path: return False, "Could not determine installation path" install_path = Path(install_path) + + # For Unix systems, migrate to new location if currently in old location + system = plat.system().lower() + if system != 'windows': + home = Path.home() + old_path = home / '.tally' / 'bin' / 'tally' + new_path = home / '.local' / 'bin' / 'tally' + + # If updating from old location, target new location instead + if current_path and current_path == old_path: + print(f"Migrating installation from {old_path} to {new_path}...") + install_path = new_path # Check if running from source (not frozen) if not getattr(__import__('sys'), 'frozen', False): return False, "Cannot self-update when running from source. Use: uv tool upgrade tally" - system = plat.system().lower() binary_name = 'tally.exe' if system == 'windows' else 'tally' try: @@ -349,8 +369,34 @@ def perform_update(release_info: dict, force: bool = False) -> tuple[bool, str]: else: # On Unix, atomic rename shutil.copy2(new_binary, install_path) - - return True, f"Updated to v{release_info['version']}" + + # Clean up old installation on Unix if we migrated + if system != 'windows' and current_path: + old_install = home / '.tally' / 'bin' / 'tally' + if current_path == old_install and install_path != old_install: + try: + if old_install.exists(): + old_install.unlink() + print(f"Removed old installation at {old_install}") + + # Remove old directory if empty + old_bin_dir = old_install.parent + if old_bin_dir.exists() and not list(old_bin_dir.iterdir()): + old_bin_dir.rmdir() + old_tally_dir = old_bin_dir.parent + if old_tally_dir.exists() and not list(old_tally_dir.iterdir()): + old_tally_dir.rmdir() + print(f"Removed old directory {old_tally_dir}") + except Exception as e: + # Don't fail the update if cleanup fails + print(f"Note: Could not clean up old installation: {e}") + + msg = f"Updated to v{release_info['version']}" + if system != 'windows' and current_path and current_path != install_path: + msg += f"\n\nInstalled to new location: {install_path}" + msg += f"\nTo complete migration, update your PATH to use ~/.local/bin instead of ~/.tally/bin" + + return True, msg except PermissionError: return False, f"Permission denied. Try running with elevated privileges or manually update at: {install_path}"