mirror of
https://github.com/veeso/termscp.git
synced 2026-06-12 19:49:49 +02:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
3
dist/release/bump_version.sh
vendored
3
dist/release/bump_version.sh
vendored
@@ -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"
|
||||
|
||||
|
||||
181
install.ps1
Normal file
181
install.ps1
Normal file
@@ -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 <https://www.rust-lang.org/tools/install>: 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 <https://github.com/veeso/termscp/issues/new>"
|
||||
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 <https://docs.termscp.rs>"
|
||||
Write-Info "Remember that if you encounter any issue, you can report them on Github <https://github.com/veeso/termscp/issues/new>"
|
||||
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
|
||||
1
site/.gitignore
vendored
1
site/.gitignore
vendored
@@ -2,3 +2,4 @@ dist/
|
||||
.astro/
|
||||
node_modules/
|
||||
public/install.sh
|
||||
public/install.ps1
|
||||
|
||||
@@ -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}`);
|
||||
}),
|
||||
);
|
||||
|
||||
54
site/src/components/CopyButton.astro
Normal file
54
site/src/components/CopyButton.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
interface Props {
|
||||
text: string;
|
||||
class?: string;
|
||||
}
|
||||
const { text, class: className } = Astro.props;
|
||||
---
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
"copy-btn rounded p-1.5 text-overlay transition-colors hover:bg-mantle hover:text-text",
|
||||
className,
|
||||
]}
|
||||
data-copy={text}
|
||||
aria-label="Copy command to clipboard"
|
||||
title="Copy"
|
||||
>
|
||||
<svg class="copy-icon h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<svg class="check-icon hidden h-4 w-4 text-green" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<script>
|
||||
const RESET_DELAY_MS = 1500;
|
||||
|
||||
const handleCopy = async (button: HTMLElement) => {
|
||||
const text = button.getAttribute("data-copy") ?? "";
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const copyIcon = button.querySelector(".copy-icon");
|
||||
const checkIcon = button.querySelector(".check-icon");
|
||||
copyIcon?.classList.add("hidden");
|
||||
checkIcon?.classList.remove("hidden");
|
||||
|
||||
window.setTimeout(() => {
|
||||
copyIcon?.classList.remove("hidden");
|
||||
checkIcon?.classList.add("hidden");
|
||||
}, RESET_DELAY_MS);
|
||||
};
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const button = (event.target as HTMLElement)?.closest<HTMLElement>(".copy-btn");
|
||||
if (button) {
|
||||
handleCopy(button);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -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;
|
||||
))}
|
||||
</div>
|
||||
{methods.map((m, i) => (
|
||||
<pre
|
||||
class="install-panel m-0 overflow-auto bg-base px-4 py-4 text-sm text-text"
|
||||
data-id={m.id}
|
||||
hidden={i !== 0}
|
||||
><code>{m.cmd}</code></pre>
|
||||
<div class="install-panel relative" data-id={m.id} hidden={i !== 0}>
|
||||
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{m.cmd}</code></pre>
|
||||
<CopyButton text={m.cmd} class="absolute right-2 top-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<script is:inline>
|
||||
|
||||
@@ -3,9 +3,11 @@ import Base from "../layouts/Base.astro";
|
||||
import Nav from "../components/Nav.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import InstallTabs from "../components/InstallTabs.astro";
|
||||
import CopyButton from "../components/CopyButton.astro";
|
||||
import { DOCS_URL } from "../consts";
|
||||
|
||||
const script = "curl --proto '=https' --tlsv1.2 -sSLf https://termscp.rs/install.sh | sh";
|
||||
const psScript = "irm https://termscp.rs/install.ps1 | iex";
|
||||
|
||||
const methods = [
|
||||
{ id: "cargo", label: "Cargo", cmd: "cargo install termscp --locked" },
|
||||
@@ -28,10 +30,24 @@ const methods = [
|
||||
<div class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay">
|
||||
<span class="text-green">$</span> Linux · macOS · BSD
|
||||
</div>
|
||||
<pre class="m-0 overflow-auto bg-base px-4 py-4 text-sm text-text"><code>{script}</code></pre>
|
||||
<div class="relative">
|
||||
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{script}</code></pre>
|
||||
<CopyButton text={script} class="absolute right-2 top-2" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-overlay">Requires <span class="text-text">curl</span>. macOS uses Homebrew if available, otherwise builds from source.</p>
|
||||
|
||||
<div class="mt-6 rounded-lg border border-line bg-mantle p-1">
|
||||
<div class="flex items-center gap-2 border-b border-line px-3 py-2 text-xs text-overlay">
|
||||
<span class="text-green">></span> Windows (PowerShell)
|
||||
</div>
|
||||
<div class="relative">
|
||||
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{psScript}</code></pre>
|
||||
<CopyButton text={psScript} class="absolute right-2 top-2" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-overlay">Downloads the latest release and adds <span class="text-text">termscp</span> to your user PATH.</p>
|
||||
|
||||
<h2 class="mt-10 mb-3 text-lg text-text">Package managers</h2>
|
||||
<InstallTabs methods={methods} />
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
{
|
||||
"source": "/install.sh",
|
||||
"headers": [{ "key": "Content-Type", "value": "text/x-shellscript; charset=utf-8" }]
|
||||
},
|
||||
{
|
||||
"source": "/install.ps1",
|
||||
"headers": [{ "key": "Content-Type", "value": "text/plain; charset=utf-8" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user