From f92cb93755580ecae0e5ee5dd4590bd497472b95 Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Sun, 7 Jun 2026 23:04:39 +0200 Subject: [PATCH] feat(install): add Windows PowerShell installer and copy buttons on site Add install.ps1 mirroring install.sh for Windows: arch detection, release zip download, binary extraction, user PATH update. - copy install.ps1 to site public/ at build time (copy-install.mjs) - serve /install.ps1 with text/plain Content-Type (vercel.json) - add PowerShell one-liner to install page and README - bump install.ps1 default version in bump_version.sh - add CopyButton component next to every install command line --- README.md | 8 +- dist/release/bump_version.sh | 3 + install.ps1 | 181 ++++++++++++++++++++++++++ site/.gitignore | 1 + site/scripts/copy-install.mjs | 30 +++-- site/src/components/CopyButton.astro | 54 ++++++++ site/src/components/InstallTabs.astro | 11 +- site/src/pages/install.astro | 18 ++- site/vercel.json | 4 + 9 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 install.ps1 create mode 100644 site/src/components/CopyButton.astro diff --git a/README.md b/README.md index e1ed5a8..cbba758 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,13 @@ curl --proto '=https' --tlsv1.2 -sSLf https://termscp.rs/install.sh | sh > ❗ MacOs installation requires [Homebrew](https://brew.sh/), otherwise the Rust compiler will be installed -while if you're a Windows user, you can install termscp with [Chocolatey](https://chocolatey.org/): +if you're a Windows user, you can install termscp from PowerShell with a single command: + +```ps +irm https://termscp.rs/install.ps1 | iex +``` + +or, alternatively, with [Chocolatey](https://chocolatey.org/): ```ps choco install termscp diff --git a/dist/release/bump_version.sh b/dist/release/bump_version.sh index 90b9434..f30ff8c 100755 --- a/dist/release/bump_version.sh +++ b/dist/release/bump_version.sh @@ -16,6 +16,9 @@ sedi "s/^version = \"[0-9][0-9A-Za-z.\\-]*\"/version = \"$VERSION\"/m" "$ROOT/Ca # install.sh — the literal default assignment only sedi "s/^TERMSCP_VERSION=\"[0-9][0-9A-Za-z.\\-]*\"/TERMSCP_VERSION=\"$VERSION\"/m" "$ROOT/install.sh" +# install.ps1 — the default -Version parameter value only +sedi "s/\\\$Version = \"[0-9][0-9A-Za-z.\\-]*\"/\\\$Version = \"$VERSION\"/" "$ROOT/install.ps1" + # README.md — version + release date sedi "s/Current version: [0-9][0-9A-Za-z.\\-]* [0-9]{4}-[0-9]{2}-[0-9]{2}/Current version: $VERSION $DATE/" "$ROOT/README.md" diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..e7ee632 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,181 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Installer for termscp on Windows. + +.DESCRIPTION + Downloads the latest (or a specified) termscp release for Windows from + GitHub, extracts the binary into an install directory and adds it to the + current user's PATH. + +.PARAMETER Version + The termscp version to install (defaults to the latest released version). + +.PARAMETER InstallDir + The directory the termscp.exe binary is installed into. + Defaults to "$env:LOCALAPPDATA\Programs\termscp". + +.PARAMETER Force + Skip the confirmation prompt during installation. Alias: -Yes. + +.EXAMPLE + irm https://termscp.rs/install.ps1 | iex + +.EXAMPLE + .\install.ps1 -Version 1.0.0 -Force +#> +[CmdletBinding()] +param( + [string]$Version = "1.0.0", + [string]$InstallDir = "$env:LOCALAPPDATA\Programs\termscp", + [Alias("Yes")] + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +$GithubUrl = "https://github.com/veeso/termscp/releases/download/v$Version" + +# -- output helpers ---------------------------------------------------------- + +function Write-Info { + param([string]$Message) + Write-Host "> " -ForegroundColor DarkGray -NoNewline + Write-Host $Message +} + +function Write-Warn { + param([string]$Message) + Write-Host "! $Message" -ForegroundColor Yellow +} + +function Write-Err { + param([string]$Message) + Write-Host "x $Message" -ForegroundColor Red +} + +function Write-Completed { + param([string]$Message) + Write-Host "✓ " -ForegroundColor Green -NoNewline + Write-Host $Message +} + +function Confirm-Action { + param([string]$Message) + if ($Force) { + return + } + $answer = Read-Host "? $Message [y/N]" + if ($answer -ne "y" -and $answer -ne "yes") { + Write-Err 'Aborting (please answer "yes" to continue)' + exit 1 + } +} + +# -- platform detection ------------------------------------------------------ + +# Currently supporting: +# - x86_64 (AMD64) +# - aarch64 (ARM64) +function Get-TermscpTarget { + $arch = $env:PROCESSOR_ARCHITECTURE + if ($env:PROCESSOR_ARCHITEW6432) { + $arch = $env:PROCESSOR_ARCHITEW6432 + } + + switch ($arch.ToUpper()) { + "AMD64" { return "x86_64-pc-windows-msvc" } + "ARM64" { return "aarch64-pc-windows-msvc" } + default { + Write-Err "Unsupported architecture: $arch" + Write-Info "Only x86_64 (AMD64) and aarch64 (ARM64) are supported by this installer." + Write-Info "Alternatively you can install termscp with Cargo : cargo install termscp --locked" + exit 1 + } + } +} + +# -- installation ------------------------------------------------------------ + +function Install-Termscp { + $target = Get-TermscpTarget + $asset = "termscp-v$Version-$target.zip" + $url = "$GithubUrl/$asset" + + Write-Host "" + Write-Host " Termscp configuration" + Write-Info "Version: $Version" + Write-Info "Target: $target" + Write-Info "Install dir: $InstallDir" + Write-Host "" + + Confirm-Action "Install termscp $Version?" + + $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "termscp-$([System.IO.Path]::GetRandomFileName())" + New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null + + try { + $archive = Join-Path $tmpDir $asset + Write-Info "Downloading termscp from $url …" + try { + Invoke-WebRequest -Uri $url -OutFile $archive -UseBasicParsing + } catch { + Write-Err "Failed to download termscp: $($_.Exception.Message)" + Write-Warn "If you believe this is a bug, please report an issue at " + exit 1 + } + + Write-Info "Extracting archive …" + Expand-Archive -Path $archive -DestinationPath $tmpDir -Force + + if (-not (Test-Path $InstallDir)) { + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + } + + $binary = Join-Path $tmpDir "termscp.exe" + if (-not (Test-Path $binary)) { + Write-Err "termscp.exe not found in the downloaded archive" + exit 1 + } + + Write-Info "Installing termscp to $InstallDir …" + Copy-Item -Path $binary -Destination (Join-Path $InstallDir "termscp.exe") -Force + + Add-ToUserPath -Directory $InstallDir + } finally { + Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +function Add-ToUserPath { + param([string]$Directory) + + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $entries = @() + if ($userPath) { + $entries = $userPath.Split(";") | Where-Object { $_ -ne "" } + } + + if ($entries -contains $Directory) { + return + } + + Write-Info "Adding $Directory to your user PATH …" + $newPath = (@($entries) + $Directory) -join ";" + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") + # make termscp available in the current session too + $env:Path = "$env:Path;$Directory" + Write-Warn "Restart your terminal for the PATH change to take effect in new sessions." +} + +# -- main -------------------------------------------------------------------- + +Install-Termscp + +Write-Completed "Congratulations! Termscp has successfully been installed on your system!" +Write-Info "If you're a new user, you might be interested in reading the user manual " +Write-Info "Remember that if you encounter any issue, you can report them on Github " +Write-Info "Feel free to open an issue also if you have an idea which could improve the project" +Write-Info "I hope you'll enjoy using termscp :D" + +exit 0 diff --git a/site/.gitignore b/site/.gitignore index 6025605..58828ba 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -2,3 +2,4 @@ dist/ .astro/ node_modules/ public/install.sh +public/install.ps1 diff --git a/site/scripts/copy-install.mjs b/site/scripts/copy-install.mjs index 9fc6bc9..d5c4d44 100644 --- a/site/scripts/copy-install.mjs +++ b/site/scripts/copy-install.mjs @@ -3,16 +3,24 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); -const src = join(here, '..', '..', 'install.sh'); // repo-root install.sh (single source of truth) -const dest = join(here, '..', 'public', 'install.sh'); -try { - await access(src); -} catch { - console.error(`[copy-install] source not found: ${src}`); - console.error('[copy-install] the repo-root install.sh must be available at build time.'); - process.exit(1); -} +// repo-root install scripts (single source of truth) +const scripts = ['install.sh', 'install.ps1']; -await copyFile(src, dest); -console.log('[copy-install] copied install.sh -> public/install.sh'); +await Promise.all( + scripts.map(async (name) => { + const src = join(here, '..', '..', name); + const dest = join(here, '..', 'public', name); + + try { + await access(src); + } catch { + console.error(`[copy-install] source not found: ${src}`); + console.error(`[copy-install] the repo-root ${name} must be available at build time.`); + process.exit(1); + } + + await copyFile(src, dest); + console.log(`[copy-install] copied ${name} -> public/${name}`); + }), +); diff --git a/site/src/components/CopyButton.astro b/site/src/components/CopyButton.astro new file mode 100644 index 0000000..21064b5 --- /dev/null +++ b/site/src/components/CopyButton.astro @@ -0,0 +1,54 @@ +--- +interface Props { + text: string; + class?: string; +} +const { text, class: className } = Astro.props; +--- + + diff --git a/site/src/components/InstallTabs.astro b/site/src/components/InstallTabs.astro index 865941d..04001b4 100644 --- a/site/src/components/InstallTabs.astro +++ b/site/src/components/InstallTabs.astro @@ -1,4 +1,6 @@ --- +import CopyButton from "./CopyButton.astro"; + interface Method { id: string; label: string; cmd: string; } interface Props { methods: Method[]; } const { methods } = Astro.props; @@ -14,11 +16,10 @@ const { methods } = Astro.props; ))} {methods.map((m, i) => ( - + ))}