mirror of
https://github.com/veeso/termscp.git
synced 2026-07-28 10:31:03 +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
|
> ❗ 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
|
```ps
|
||||||
choco install termscp
|
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
|
# install.sh — the literal default assignment only
|
||||||
sedi "s/^TERMSCP_VERSION=\"[0-9][0-9A-Za-z.\\-]*\"/TERMSCP_VERSION=\"$VERSION\"/m" "$ROOT/install.sh"
|
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
|
# 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"
|
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/
|
.astro/
|
||||||
node_modules/
|
node_modules/
|
||||||
public/install.sh
|
public/install.sh
|
||||||
|
public/install.ps1
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ import { dirname, join } from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.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 {
|
// repo-root install scripts (single source of truth)
|
||||||
await access(src);
|
const scripts = ['install.sh', 'install.ps1'];
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
await copyFile(src, dest);
|
await Promise.all(
|
||||||
console.log('[copy-install] copied install.sh -> public/install.sh');
|
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 Method { id: string; label: string; cmd: string; }
|
||||||
interface Props { methods: Method[]; }
|
interface Props { methods: Method[]; }
|
||||||
const { methods } = Astro.props;
|
const { methods } = Astro.props;
|
||||||
@@ -14,11 +16,10 @@ const { methods } = Astro.props;
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{methods.map((m, i) => (
|
{methods.map((m, i) => (
|
||||||
<pre
|
<div class="install-panel relative" data-id={m.id} hidden={i !== 0}>
|
||||||
class="install-panel m-0 overflow-auto bg-base px-4 py-4 text-sm text-text"
|
<pre class="m-0 overflow-auto bg-base px-4 py-4 pr-12 text-sm text-text"><code>{m.cmd}</code></pre>
|
||||||
data-id={m.id}
|
<CopyButton text={m.cmd} class="absolute right-2 top-2" />
|
||||||
hidden={i !== 0}
|
</div>
|
||||||
><code>{m.cmd}</code></pre>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import Base from "../layouts/Base.astro";
|
|||||||
import Nav from "../components/Nav.astro";
|
import Nav from "../components/Nav.astro";
|
||||||
import Footer from "../components/Footer.astro";
|
import Footer from "../components/Footer.astro";
|
||||||
import InstallTabs from "../components/InstallTabs.astro";
|
import InstallTabs from "../components/InstallTabs.astro";
|
||||||
|
import CopyButton from "../components/CopyButton.astro";
|
||||||
import { DOCS_URL } from "../consts";
|
import { DOCS_URL } from "../consts";
|
||||||
|
|
||||||
const script = "curl --proto '=https' --tlsv1.2 -sSLf https://termscp.rs/install.sh | sh";
|
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 = [
|
const methods = [
|
||||||
{ id: "cargo", label: "Cargo", cmd: "cargo install termscp --locked" },
|
{ 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">
|
<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
|
<span class="text-green">$</span> Linux · macOS · BSD
|
||||||
</div>
|
</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>
|
</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>
|
<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>
|
<h2 class="mt-10 mb-3 text-lg text-text">Package managers</h2>
|
||||||
<InstallTabs methods={methods} />
|
<InstallTabs methods={methods} />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
{
|
{
|
||||||
"source": "/install.sh",
|
"source": "/install.sh",
|
||||||
"headers": [{ "key": "Content-Type", "value": "text/x-shellscript; charset=utf-8" }]
|
"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