Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 54 additions & 6 deletions docs/install-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -156,28 +204,28 @@ 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

# Create config file directory if needed
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

Expand Down
60 changes: 54 additions & 6 deletions docs/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -150,28 +198,28 @@ 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

# Create config file directory if needed
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

Expand Down
56 changes: 51 additions & 5 deletions src/tally/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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}"
Expand Down
Loading