Skip to content

Commit ff2e647

Browse files
kylemclarenclaude
andcommitted
Add dynamic CLI download cards with build-time version fetching
- Create DownloadCards React component with platform selection cards - Create CliDownloads Astro component that renders proper code blocks - Add cli-releases utility to fetch latest RC version from S3 bucket - Platform cards show macOS/Linux/Windows with download buttons - Selecting a platform shows installation instructions with proper Starlight code blocks (copy button, syntax highlighting) - Version is fetched at build time, no hardcoded URLs Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 932ee74 commit ff2e647

File tree

5 files changed

+369
-19
lines changed

5 files changed

+369
-19
lines changed

src/components/CliDownloads.astro

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
import { Code } from '@astrojs/starlight/components';
3+
import { DownloadCards } from '@/components/react';
4+
import { getLatestRcRelease } from '@/lib/cli-releases';
5+
6+
const release = await getLatestRcRelease();
7+
8+
// Get specific binaries for examples
9+
const darwinArm64 = release.binaries.find(b => b.filename.includes('darwin-arm64'))!;
10+
const linuxAmd64 = release.binaries.find(b => b.filename.includes('linux-amd64'))!;
11+
const windowsAmd64 = release.binaries.find(b => b.filename.includes('windows-amd64'))!;
12+
13+
const instructions = [
14+
{
15+
platform: 'macOS',
16+
steps: [
17+
{
18+
label: 'Download the binary',
19+
code: `curl -LO ${darwinArm64.url}`,
20+
},
21+
{
22+
label: 'Verify checksum (recommended)',
23+
code: `curl -LO ${darwinArm64.checksumUrl}
24+
shasum -a 256 -c ${darwinArm64.filename}.sha256`,
25+
},
26+
{
27+
label: 'Extract and install',
28+
code: `tar xzf ${darwinArm64.filename}
29+
sudo mv sprite /usr/local/bin/`,
30+
},
31+
],
32+
note: 'For Intel Macs, replace "arm64" with "amd64" in the URLs above.',
33+
},
34+
{
35+
platform: 'Linux',
36+
steps: [
37+
{
38+
label: 'Download the binary',
39+
code: `curl -LO ${linuxAmd64.url}`,
40+
},
41+
{
42+
label: 'Verify checksum (recommended)',
43+
code: `curl -LO ${linuxAmd64.checksumUrl}
44+
sha256sum -c ${linuxAmd64.filename}.sha256`,
45+
},
46+
{
47+
label: 'Extract and install',
48+
code: `tar xzf ${linuxAmd64.filename}
49+
sudo mv sprite /usr/local/bin/`,
50+
},
51+
],
52+
note: 'For ARM64, replace "amd64" with "arm64" in the URLs above.',
53+
},
54+
{
55+
platform: 'Windows',
56+
steps: [
57+
{
58+
label: 'Download the binary',
59+
code: `Invoke-WebRequest -Uri "${windowsAmd64.url}" -OutFile "${windowsAmd64.filename}"`,
60+
},
61+
{
62+
label: 'Extract to your bin directory',
63+
code: `Expand-Archive ${windowsAmd64.filename} -DestinationPath $env:USERPROFILE\\bin`,
64+
},
65+
{
66+
label: 'Add to PATH (run as Administrator)',
67+
code: `[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$env:USERPROFILE\\bin", "User")`,
68+
},
69+
],
70+
note: 'For ARM64 Windows, replace "amd64" with "arm64" in the URL above.',
71+
},
72+
];
73+
---
74+
75+
<div class="cli-downloads" data-default-platform="macOS">
76+
<DownloadCards
77+
client:load
78+
binaries={release.binaries}
79+
version={release.version}
80+
/>
81+
82+
{instructions.map((instruction) => (
83+
<div class="platform-instructions" data-platform={instruction.platform}>
84+
{instruction.steps.map((step) => (
85+
<div class="instruction-step">
86+
<p class="step-label">{step.label}</p>
87+
<Code code={step.code} lang={instruction.platform === 'Windows' ? 'powershell' : 'bash'} />
88+
</div>
89+
))}
90+
{instruction.note && (
91+
<p class="step-note">{instruction.note}</p>
92+
)}
93+
</div>
94+
))}
95+
</div>
96+
97+
<style>
98+
.platform-instructions {
99+
display: none;
100+
margin-top: 2rem;
101+
}
102+
103+
.platform-instructions.active {
104+
display: block;
105+
}
106+
107+
.instruction-step {
108+
margin-bottom: 1.25rem;
109+
}
110+
111+
.step-label {
112+
font-size: 0.875rem;
113+
font-weight: 500;
114+
margin-bottom: 0.5rem;
115+
}
116+
117+
.step-note {
118+
font-size: 0.875rem;
119+
color: var(--sl-color-gray-3);
120+
font-style: italic;
121+
margin-top: 0.5rem;
122+
}
123+
</style>
124+
125+
<script is:inline>
126+
(function() {
127+
function initCliDownloads() {
128+
document.querySelectorAll('.cli-downloads').forEach(function(container) {
129+
var defaultPlatform = container.getAttribute('data-default-platform') || 'macOS';
130+
131+
// Show default platform instructions
132+
var defaultInstructions = container.querySelector('[data-platform="' + defaultPlatform + '"]');
133+
if (defaultInstructions) {
134+
defaultInstructions.classList.add('active');
135+
}
136+
});
137+
}
138+
139+
// Listen for platform changes from DownloadCards
140+
document.addEventListener('cli-platform-changed', function(event) {
141+
var platform = event.detail.platform;
142+
143+
document.querySelectorAll('.cli-downloads').forEach(function(container) {
144+
// Hide all instructions
145+
container.querySelectorAll('.platform-instructions').forEach(function(el) {
146+
el.classList.remove('active');
147+
});
148+
149+
// Show selected platform
150+
var selected = container.querySelector('[data-platform="' + platform + '"]');
151+
if (selected) {
152+
selected.classList.add('active');
153+
}
154+
});
155+
});
156+
157+
// Run on page load
158+
initCliDownloads();
159+
})();
160+
</script>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Download } from 'lucide-react';
2+
import { useEffect, useState } from 'react';
3+
import { Button } from '@/components/ui/button';
4+
import { Item, ItemContent, ItemMedia, ItemTitle } from '@/components/ui/item';
5+
import { cn } from '@/lib/utils';
6+
7+
function AppleIcon(): React.ReactElement {
8+
return (
9+
<svg viewBox="0 0 24 24" fill="currentColor" className="size-8">
10+
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
11+
</svg>
12+
);
13+
}
14+
15+
function LinuxIcon(): React.ReactElement {
16+
return (
17+
<svg viewBox="0 0 24 24" fill="currentColor" className="size-8">
18+
<path d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.025c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.117-.468.31-.774.64-.934a1.22 1.22 0 01.187-.06c-.015.06-.039.12-.062.18-.08.334.104.7.263.85z" />
19+
</svg>
20+
);
21+
}
22+
23+
function WindowsIcon(): React.ReactElement {
24+
return (
25+
<svg viewBox="0 0 24 24" fill="currentColor" className="size-8">
26+
<path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801" />
27+
</svg>
28+
);
29+
}
30+
31+
const PLATFORM_ICONS: Record<string, () => React.ReactElement> = {
32+
macOS: AppleIcon,
33+
Linux: LinuxIcon,
34+
Windows: WindowsIcon,
35+
};
36+
37+
interface Binary {
38+
platform: string;
39+
arch: string;
40+
filename: string;
41+
url: string;
42+
}
43+
44+
interface DownloadCardsProps {
45+
binaries: Binary[];
46+
version: string;
47+
}
48+
49+
function groupByPlatform(binaries: Binary[]): Record<string, Binary[]> {
50+
const grouped: Record<string, Binary[]> = {};
51+
for (const binary of binaries) {
52+
if (!grouped[binary.platform]) {
53+
grouped[binary.platform] = [];
54+
}
55+
grouped[binary.platform].push(binary);
56+
}
57+
return grouped;
58+
}
59+
60+
function DownloadCards({
61+
binaries,
62+
version,
63+
}: DownloadCardsProps): React.ReactElement {
64+
const [selectedPlatform, setSelectedPlatform] = useState('macOS');
65+
66+
useEffect(() => {
67+
const event = new CustomEvent('cli-platform-changed', {
68+
detail: { platform: selectedPlatform },
69+
});
70+
document.dispatchEvent(event);
71+
}, [selectedPlatform]);
72+
73+
const grouped = groupByPlatform(binaries);
74+
75+
return (
76+
<div className="not-content">
77+
<p className="text-sm text-muted-foreground mb-4">
78+
Latest release: <strong className="text-foreground">{version}</strong>
79+
</p>
80+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
81+
{Object.entries(grouped).map(([platform, platformBinaries]) => {
82+
const IconComponent = PLATFORM_ICONS[platform];
83+
const isSelected = platform === selectedPlatform;
84+
return (
85+
<Item
86+
key={platform}
87+
variant="outline"
88+
className={cn(
89+
'flex-col items-center text-center px-4 py-3 rounded-lg cursor-pointer transition-colors',
90+
isSelected
91+
? 'border-primary bg-primary/5'
92+
: 'hover:border-muted-foreground/50',
93+
)}
94+
onClick={() => setSelectedPlatform(platform)}
95+
>
96+
<ItemMedia
97+
className={cn(
98+
'mb-1 transition-colors',
99+
isSelected ? 'text-primary' : 'text-muted-foreground',
100+
)}
101+
>
102+
{IconComponent && <IconComponent />}
103+
</ItemMedia>
104+
<ItemContent className="items-center gap-2">
105+
<ItemTitle className="text-sm">{platform}</ItemTitle>
106+
<div className="flex gap-1">
107+
{platformBinaries.map((binary) => (
108+
<Button
109+
key={binary.filename}
110+
variant="ghost"
111+
size="sm"
112+
asChild
113+
className="h-7 px-2 text-xs"
114+
>
115+
<a
116+
href={binary.url}
117+
className="no-underline"
118+
onClick={(e) => e.stopPropagation()}
119+
>
120+
<Download size={12} />
121+
{binary.arch}
122+
</a>
123+
</Button>
124+
))}
125+
</div>
126+
</ItemContent>
127+
</Item>
128+
);
129+
})}
130+
</div>
131+
</div>
132+
);
133+
}
134+
135+
export default DownloadCards;

src/components/react/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export { default as CardGrid } from './CardGrid';
2626
export { CodeTabs, Snippet } from './CodeTabs';
2727
export { CopyPageButton } from './CopyPageButton';
2828
export { DotPattern } from './DotPattern';
29+
export { default as DownloadCards } from './DownloadCards';
2930
export { LifecycleDiagram } from './LifecycleDiagram';
3031
export { default as LinkCard } from './LinkCard';
3132
export { Pagination } from './Pagination';

src/content/docs/cli/installation.mdx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description: Install the Sprites CLI on macOS, Linux, or Windows
44
---
55

66
import { Callout, LinkCard, CardGrid } from '@/components/react';
7+
import CliDownloads from '@/components/CliDownloads.astro';
78

89
The Sprites CLI (`sprite`) is available for macOS, Linux, and Windows.
910

@@ -27,25 +28,7 @@ After installation, you may need to add `~/.local/bin` to your PATH if it's not
2728

2829
## Manual Installation
2930

30-
For specific versions or manual installation, download binaries from [GitHub Releases](https://github.com/superfly/sprite-env/releases).
31-
32-
### macOS / Linux
33-
34-
```bash
35-
# Download and extract the appropriate binary for your platform
36-
tar xzf sprite-<platform>-<arch>.tar.gz
37-
sudo mv sprite /usr/local/bin/
38-
```
39-
40-
### Windows
41-
42-
```powershell
43-
# Download and extract
44-
Expand-Archive sprite-windows-amd64.zip -DestinationPath $env:USERPROFILE\bin
45-
46-
# Add to PATH (run in PowerShell as Administrator)
47-
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$env:USERPROFILE\bin", "User")
48-
```
31+
<CliDownloads />
4932

5033
## Verify Installation
5134

0 commit comments

Comments
 (0)