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:
Christian Visintin
2026-06-07 23:04:39 +02:00
parent d070825dd7
commit f92cb93755
9 changed files with 292 additions and 18 deletions

1
site/.gitignore vendored
View File

@@ -2,3 +2,4 @@ dist/
.astro/
node_modules/
public/install.sh
public/install.ps1

View File

@@ -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}`);
}),
);

View 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>

View File

@@ -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>

View File

@@ -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">&gt;</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} />

View File

@@ -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" }]
}
]
}