Skip to content
Open
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
112 changes: 111 additions & 1 deletion public/scripts/install_cli.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,43 @@ $BIN_DIR = "$env:USERPROFILE\.aptoscli\bin"
$FORCE = $false
$ACCEPT_ALL = $false
$VERSION = ""
$UNDO = $false

# Show usage information
function Show-Usage {
Write-Host @"
Usage: install_cli.ps1 [OPTIONS]

Installs the latest version of the Aptos CLI on Windows.

During upgrades, the installer automatically backs up the current binary
so you can roll back with --undo. If the upgrade crosses a major version
boundary (e.g. v1.x.x -> v2.x.x), a warning is displayed with a link to
the CHANGELOG for breaking changes:
https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md

OPTIONS:
-f, --force Install even if the same version is already installed
-y, --yes Accept all prompts automatically
--bin-dir DIR Install the CLI binary to DIR
(default: %USERPROFILE%\.aptoscli\bin)
--cli-version VER Install a specific version instead of the latest
--undo Restore the previous CLI version from the backup
created during the last upgrade. Only one backup is
kept at a time. No network access is required.
-h, --help Show this help message and exit

EXAMPLES:
# Install the latest version
.\install_cli.ps1

# Install a specific version
.\install_cli.ps1 --cli-version 3.5.0

# Roll back to the previously installed version
.\install_cli.ps1 --undo
"@
}

# Print colored message
function Write-ColorMessage {
Expand All @@ -44,6 +81,60 @@ function Test-CommandExists {
return [bool](Get-Command -Name $Command -ErrorAction SilentlyContinue)
}

# Check for major version upgrade and warn user
function Test-MajorVersionUpgrade {
param(
[string]$OldVersion,
[string]$NewVersion
)

if (-not $OldVersion -or -not $NewVersion) {
return
}

$oldMajor = $OldVersion.Split('.')[0]
$newMajor = $NewVersion.Split('.')[0]

if ($oldMajor -ne $newMajor) {
Write-Host ""
Write-ColorMessage -Color $YELLOW -Message "WARNING: This is a major version upgrade (v${oldMajor}.x.x -> v${newMajor}.x.x)."
Write-ColorMessage -Color $YELLOW -Message "Major version upgrades may include breaking changes."
Write-ColorMessage -Color $YELLOW -Message "Please review the CHANGELOG before continuing:"
Write-ColorMessage -Color $CYAN -Message " https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md"
Write-Host ""
}
}

# Backup the current CLI binary before overwriting (keeps only one backup)
function Backup-CurrentBinary {
$cliPath = Join-Path $BIN_DIR $SCRIPT
if (Test-Path $cliPath) {
$backupPath = "$cliPath.backup"
Write-ColorMessage -Color $CYAN -Message "Backing up current CLI binary to $backupPath..."
Copy-Item -Path $cliPath -Destination $backupPath -Force
Write-ColorMessage -Color $GREEN -Message "Backup complete. Use --undo to restore the previous version."
}
}

# Restore the previously backed up CLI binary
function Undo-Upgrade {
$cliPath = Join-Path $BIN_DIR $SCRIPT
$backupPath = "$cliPath.backup"
if (-not (Test-Path $backupPath)) {
Die "No backup found at $backupPath. Nothing to undo."
}

Write-ColorMessage -Color $CYAN -Message "Restoring previous CLI version..."
Move-Item -Path $backupPath -Destination $cliPath -Force

$restoredVersion = (& $cliPath --version | Select-String -Pattern '\d+\.\d+\.\d+').Matches.Value
if ($restoredVersion) {
Write-ColorMessage -Color $GREEN -Message "Successfully restored Aptos CLI version $restoredVersion."
} else {
Write-ColorMessage -Color $GREEN -Message "Previous CLI version restored."
}
}

# Get the latest version from GitHub API
function Get-LatestVersion {
try {
Expand Down Expand Up @@ -94,6 +185,9 @@ function Install-CLI {
curl.exe -L $url -o $zipPath
}

# Backup current binary before overwriting
Backup-CurrentBinary

# Extract the zip file
Expand-Archive -Path $zipPath -DestinationPath $BIN_DIR -Force

Expand Down Expand Up @@ -134,11 +228,26 @@ function Main {
Die "No version specified for --cli-version"
}
}
'--undo' { $UNDO = $true }
'-h' {
Show-Usage
return
}
'--help' {
Show-Usage
return
}
default {
Die "Unknown option: $($args[$i])"
Die "Unknown option: $($args[$i]). Use --help for usage information."
}
}
}

# Handle undo (no network needed)
if ($UNDO) {
Undo-Upgrade
return
}

# Get version if not specified
if (-not $VERSION) {
Expand All @@ -156,6 +265,7 @@ function Main {
Write-ColorMessage -Color $YELLOW -Message "Aptos CLI version $VERSION is already installed."
return
}
Test-MajorVersionUpgrade -OldVersion $currentVersion -NewVersion $VERSION
}

# Install the CLI
Expand Down
132 changes: 126 additions & 6 deletions public/scripts/install_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,14 @@ def __init__(
accept_all: bool = False,
bin_dir: Optional[str] = None,
from_source: bool = False,
undo: bool = False,
) -> None:
self._version = version
self._force = force
self._accept_all = accept_all
self._bin_dir = Path(bin_dir).expanduser() if bin_dir else None
self._from_source = from_source
self._undo = undo

self._release_info = None
self._latest_release_info = None
Expand All @@ -268,6 +270,10 @@ def bin_dir(self) -> Path:
def bin_path(self):
return self.bin_dir.joinpath(SCRIPT)

@property
def backup_path(self):
return self.bin_dir.joinpath(SCRIPT + ".backup")

@property
def release_info(self):
if not self._release_info:
Expand All @@ -283,6 +289,10 @@ def latest_release_info(self):
raise RuntimeError("Failed to find latest CLI release")

def run(self) -> int:
# Handle undo (no network needed)
if self._undo:
return self._undo_upgrade()

# Handle installation from source
if self._from_source:
return self.run_from_source()
Expand Down Expand Up @@ -322,6 +332,10 @@ def install(self, version, target):
self._install_comment(version, "Downloading...")

self.bin_dir.mkdir(parents=True, exist_ok=True)

# Backup current binary before overwriting
self._backup_current_binary()

if self.bin_path.exists():
self.bin_path.unlink()

Expand Down Expand Up @@ -441,6 +455,80 @@ def display_post_message_unix(self, version: str) -> None:
)
)

def _backup_current_binary(self) -> None:
"""Backup the current CLI binary before overwriting (keeps only one backup)."""
if self.bin_path.exists():
self._write(
colorize("info", f"Backing up current CLI binary to {self.backup_path}...")
)
shutil.copy2(self.bin_path, self.backup_path)
self._write(
colorize("success", "Backup complete. Use --undo to restore the previous version.")
)

def _undo_upgrade(self) -> int:
"""Restore the previously backed up CLI binary."""
if not self.backup_path.exists():
self._write(
colorize("error", f"No backup found at {self.backup_path}. Nothing to undo.")
)
return 1

self._write(colorize("info", "Restoring previous CLI version..."))

# Replace current binary with backup
if self.bin_path.exists():
self.bin_path.unlink()
shutil.move(str(self.backup_path), str(self.bin_path))
os.chmod(self.bin_path, 0o755)

try:
out = subprocess.check_output(
[self.bin_path, "--version"],
universal_newlines=True,
)
restored_version = out.split(" ")[-1].rstrip().lstrip()
self._write(
colorize("success", f"Successfully restored Aptos CLI version {restored_version}.")
)
except Exception:
self._write(colorize("success", "Previous CLI version restored."))

return 0

def _warn_major_upgrade(self, current_version: str, new_version: str) -> None:
"""Warn the user if this is a major version upgrade."""
if not current_version or not new_version:
return

try:
current_major = current_version.split(".")[0]
new_major = new_version.split(".")[0]
except (IndexError, AttributeError):
return

if current_major != new_major:
self._write("")
self._write(
colorize(
"warning",
f"WARNING: This is a major version upgrade (v{current_major}.x.x -> v{new_major}.x.x).",
)
Comment on lines +512 to +516
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message says “major version upgrade”, but the check only tests inequality of major components, so it will also fire for major downgrades. Consider adjusting wording to “major version change” or computing direction (upgrade vs downgrade) for a more accurate message.

Copilot uses AI. Check for mistakes.
)
self._write(
colorize("warning", "Major version upgrades may include breaking changes.")
)
self._write(
colorize("warning", "Please review the CHANGELOG before continuing:")
)
self._write(
colorize(
"info",
" https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md",
)
)
self._write("")

def get_version(self):
if self._version:
version_to_install = self._version
Expand Down Expand Up @@ -484,6 +572,8 @@ def get_version(self):

return None, current_version
else:
if current_version:
self._warn_major_upgrade(current_version, version_to_install)
self._write(f"Installing {colorize('b', version_to_install)}")
Comment on lines +575 to 577
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--force returns early from get_version() without determining current_version, which means the new major-version warning will never be shown for forced installs. If the warning is intended whenever an existing CLI is being overwritten, consider still probing the current version (best-effort) and calling _warn_major_upgrade even in the --force path.

Copilot uses AI. Check for mistakes.

return version_to_install, current_version
Expand Down Expand Up @@ -772,6 +862,7 @@ def install_from_source(self, version: Optional[str] = None) -> int:
if current_version == latest_version:
self._write(colorize("warning", f"Aptos CLI version {latest_version} is already installed."))
return 0
self._warn_major_upgrade(current_version, latest_version)
except (FileNotFoundError, PermissionError, subprocess.CalledProcessError, OSError):
pass # CLI not installed, proceed

Expand Down Expand Up @@ -826,6 +917,9 @@ def install_from_source(self, version: Optional[str] = None) -> int:
# Move the binary to the bin directory
dest_binary = self.bin_path
try:
# Backup current binary before overwriting
self._backup_current_binary()

if dest_binary.exists():
dest_binary.unlink()
shutil.copy2(source_binary, dest_binary)
Expand Down Expand Up @@ -876,6 +970,7 @@ def run_from_source(self) -> int:
if current_version == version:
self._write(colorize("warning", f"Aptos CLI version {version} is already installed."))
return 0
self._warn_major_upgrade(current_version, version)
except (FileNotFoundError, PermissionError, subprocess.CalledProcessError, OSError):
# CLI not installed or not runnable, proceed with installation
pass
Expand All @@ -892,34 +987,58 @@ def main():
return 1

parser = argparse.ArgumentParser(
description="Installs the latest version of the Aptos CLI"
description="Installs the latest version of the Aptos CLI.",
epilog=(
"UPGRADE BEHAVIOR:\n"
" During upgrades, the installer automatically backs up the current\n"
" binary so you can roll back with --undo. If the upgrade crosses a\n"
" major version boundary (e.g. v1.x.x -> v2.x.x), a warning is\n"
" displayed with a link to the CHANGELOG for breaking changes:\n"
" https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md\n"
"\n"
"EXAMPLES:\n"
" python install_cli.py # Install latest version\n"
" python install_cli.py --cli-version 3.5.0 # Install specific version\n"
" python install_cli.py --undo # Roll back last upgrade\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-f",
"--force",
help="Forcibly install on top of existing version",
help="install even if the same version is already installed",
action="store_true",
default=False,
)
parser.add_argument(
"-y",
"--yes",
help="Accept all prompts",
help="accept all prompts automatically",
dest="accept_all",
action="store_true",
default=False,
)
parser.add_argument(
"--bin-dir",
help="If given, the CLI binary will be downloaded here instead",
help="install the CLI binary to this directory instead of the default",
)
parser.add_argument(
"--cli-version",
help="If given, the CLI version to install",
help="install a specific CLI version instead of the latest",
)
parser.add_argument(
"--from-source",
help="Build and install from source instead of downloading pre-built binary",
help="build and install from source instead of downloading a pre-built binary (requires git)",
action="store_true",
default=False,
)
parser.add_argument(
"--undo",
help=(
"restore the previous CLI version from the backup created during "
"the last upgrade. Only one backup is kept at a time. "
"No network access is required"
),
action="store_true",
default=False,
)
Expand All @@ -932,6 +1051,7 @@ def main():
bin_dir=args.bin_dir,
version=args.cli_version,
from_source=args.from_source,
undo=args.undo,
)

try:
Expand Down
Loading