release: 0.1.5 (#134)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba0860f8d1
commit
6664edf890
44 files changed
+596
-151
No files matched your search
@@ -20,9 +20,7 @@ const ROTATE_MS = 6000;
|
||||
* freshest news goes at the front.
|
||||
*/
|
||||
const ITEMS = [
|
||||
{ key: "freeModels", to: "/blog/free-models-in-penguin-harness" },
|
||||
{ key: "gemini", to: "/blog/gemini-3-6-in-penguinharness" },
|
||||
{ key: "models", to: "/blog/introducing-penguinharness" },
|
||||
{ key: "k3AndFree", to: "/blog/free-models-in-penguin-harness" },
|
||||
{ key: "fireworks", to: "/blog/fireworks-credits-amd" },
|
||||
] as const;
|
||||
|
||||
@@ -34,9 +32,7 @@ function prefersReducedMotion(): boolean {
|
||||
|
||||
export function AnnouncementBar() {
|
||||
const texts: Record<(typeof ITEMS)[number]["key"], string> = {
|
||||
freeModels: S.announcement.freeModels,
|
||||
gemini: S.announcement.gemini,
|
||||
models: S.announcement.models,
|
||||
k3AndFree: S.announcement.k3AndFree,
|
||||
fireworks: S.announcement.fireworks,
|
||||
};
|
||||
// pos runs 0..ITEMS.length where ITEMS.length is the clone of slide 0.
|
||||
|
||||
@@ -24,6 +24,24 @@ export const INSTALL_CMD = "curl -fsSL https://penguin.ooo/install.sh | sh";
|
||||
*/
|
||||
export const INSTALL_CMD_WINDOWS = "irm https://penguin.ooo/install.ps1 | iex";
|
||||
|
||||
/**
|
||||
* Offline-bundle install commands, per OS tab of the install switcher. Each Release
|
||||
* attaches five self-contained bundles (see scripts/package-offline-bundles.sh); the
|
||||
* commands show the most common architecture — the localized hint strings name the
|
||||
* alternative archive. Language-neutral, like the one-liners above.
|
||||
*/
|
||||
export const OFFLINE_INSTALL_CMDS: Record<"linux" | "macos" | "windows", string> = {
|
||||
linux: `mkdir penguin-offline
|
||||
tar -xzf penguin-linux-x64-offline.tar.gz -C penguin-offline
|
||||
./penguin-offline/install.sh`,
|
||||
macos: `mkdir penguin-offline
|
||||
tar -xzf penguin-darwin-arm64-offline.tar.gz -C penguin-offline
|
||||
./penguin-offline/install.sh`,
|
||||
windows: `Expand-Archive penguin-win32-x64-offline.zip -DestinationPath penguin-offline
|
||||
cd penguin-offline
|
||||
.\\install.cmd`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Heavy marketing media lives in the sibling `penguin-harness-community` repo rather than
|
||||
* in this one, so that assets only the landing site ever renders stay out of the clone of
|
||||
|
||||
@@ -12,9 +12,7 @@ export const en: Strings = {
|
||||
label: "Announcements",
|
||||
prev: "Previous announcement",
|
||||
next: "Next announcement",
|
||||
freeModels: "Free models Ling 3.0 Flash and the Free Models Router are now in PenguinHarness",
|
||||
gemini: "Gemini 3.6 Flash is now available in PenguinHarness",
|
||||
models: "Kimi K3 and Qwen 3.8 Max are now available in PenguinHarness",
|
||||
k3AndFree: "Kimi K3 and free models like Ling 3.0 Flash are now available in PenguinHarness",
|
||||
fireworks: "Claim $50 in Fireworks API credits with the AMD Developer Program",
|
||||
},
|
||||
|
||||
@@ -48,20 +46,16 @@ export const en: Strings = {
|
||||
},
|
||||
|
||||
hero: {
|
||||
badge: "Agents building agents",
|
||||
// The trailing space lives in the PREFIX (a breakable space before the nowrap
|
||||
// span) — inside titleNoWrap it would glue "Builder" to the unbreakable chunk.
|
||||
titlePrefix: "Your Automated Agent Builder ",
|
||||
titleNoWrap: "Lives on Your ",
|
||||
// span) — inside titleNoWrap it would glue "Builder," to the unbreakable chunk.
|
||||
titlePrefix: "Your Automated Agent Builder, ",
|
||||
titleNoWrap: "Right on Your ",
|
||||
titleWords: ["Desktop", "Server"],
|
||||
titleSuffix: "",
|
||||
keywords: ["Lightweight", "Efficient", "Open Source"],
|
||||
subtitle: "Create Self-Evolving Agents in One Click",
|
||||
ctaPrimary: "Get started",
|
||||
ctaGithub: "GitHub",
|
||||
installHint:
|
||||
"One-line install (Linux / macOS / Windows, bundled Node runtime — unpack and run)",
|
||||
installLabelPosix: "Linux / macOS",
|
||||
installLabelWindows: "Windows",
|
||||
installHint: "One-line install (bundled Node runtime — unpack and run)",
|
||||
stats: [
|
||||
{ value: "1000+", label: "supported models" },
|
||||
{ value: "1×CPU", label: "minimum footprint" },
|
||||
@@ -70,6 +64,23 @@ export const en: Strings = {
|
||||
],
|
||||
},
|
||||
|
||||
install: {
|
||||
linux: "Linux",
|
||||
macos: "macOS",
|
||||
windows: "Windows",
|
||||
online: "Online install",
|
||||
offline: "Offline package",
|
||||
offlineNote:
|
||||
"Every GitHub Release attaches five self-contained offline bundles (Linux / macOS in x64 and arm64, Windows in x64), each carrying the program archive, its SHA256 checksum and the installer: download on a networked machine, copy to the target, and install with no network at all.",
|
||||
offlineHints: {
|
||||
linux: "On arm64 machines, use penguin-linux-arm64-offline.tar.gz.",
|
||||
macos:
|
||||
"Apple silicon uses the arm64 bundle; on Intel, use penguin-darwin-x64-offline.tar.gz.",
|
||||
windows: "After unzipping you can also just double-click install.cmd.",
|
||||
},
|
||||
offlineRelease: "Download offline packages from GitHub Releases",
|
||||
},
|
||||
|
||||
copy: {
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
@@ -105,7 +116,7 @@ export const en: Strings = {
|
||||
|
||||
compare: {
|
||||
eyebrow: "vs. LangChain",
|
||||
title: "The gap between 1× and 100×",
|
||||
title: "Building Agents with Agents",
|
||||
subtitle: [
|
||||
"With LangChain, you build agents by hand — at 1× speed.",
|
||||
"With PenguinHarness, agents build agents — at 100×.",
|
||||
@@ -157,9 +168,7 @@ export const en: Strings = {
|
||||
"Install with one command and let the Agent work from a desktop-grade interface — all data stays in your local ~/.penguin/data directory.",
|
||||
step1: "Install",
|
||||
step1Desc:
|
||||
"Linux / macOS / Windows with a bundled Node runtime — unpack and run; upgrades never touch your data.",
|
||||
installLabelPosix: "Linux / macOS",
|
||||
installLabelWindows: "Windows (PowerShell)",
|
||||
"Pick your OS and method — the online one-liner, or an offline package; the bundle carries its own Node runtime, and upgrades never touch your data.",
|
||||
tabWeb: "Web UI",
|
||||
tabCli: "CLI",
|
||||
webStep2: "Open the web interface",
|
||||
|
||||
@@ -13,9 +13,7 @@ export const zh = {
|
||||
label: "公告",
|
||||
prev: "上一条公告",
|
||||
next: "下一条公告",
|
||||
freeModels: "Ling 3.0 Flash 与 Free Models Router 等免费模型现已在 PenguinHarness 可用",
|
||||
gemini: "Gemini 3.6 Flash 现已在 PenguinHarness 可用",
|
||||
models: "Kimi K3 与 Qwen 3.8 Max 模型现已在 PenguinHarness 可用",
|
||||
k3AndFree: "Kimi K3 与 Ling 3.0 Flash 等免费模型现已在 PenguinHarness 可用",
|
||||
fireworks: "携手 AMD 开发者计划:$50 Fireworks API 额度免费领取中",
|
||||
},
|
||||
|
||||
@@ -49,23 +47,20 @@ export const zh = {
|
||||
},
|
||||
|
||||
hero: {
|
||||
badge: "让 Agent 为你构建 Agent",
|
||||
/**
|
||||
* Headline: {titlePrefix}<nowrap>{titleNoWrap}{word}{titleSuffix}</nowrap>, the word
|
||||
* rotating through titleWords (桌面/服务器 — where the builder lives; the README
|
||||
* states Desktop only). The nowrap span pins the wrap point in front of it, so a
|
||||
* break never splits "Agent Builder" / "Lives on Your Desktop" mid-phrase.
|
||||
* rotating through titleWords (桌面/服务器 — where the builder runs; the README
|
||||
* writes the pair as "桌面 / 服务器"). The nowrap span pins the wrap point in front
|
||||
* of it, so a break never splits "Agent Builder" / "Right on Your Desktop" mid-phrase.
|
||||
*/
|
||||
titlePrefix: "全自动 Agent 构建器,",
|
||||
titleNoWrap: "住在你的",
|
||||
titlePrefix: "全自动 Agent 构建平台,",
|
||||
titleNoWrap: "运行在你的",
|
||||
titleWords: ["桌面", "服务器"],
|
||||
titleSuffix: "上",
|
||||
keywords: ["轻量", "高效", "开源"],
|
||||
subtitle: "一键创建自进化 Agent",
|
||||
ctaPrimary: "快速开始",
|
||||
ctaGithub: "GitHub",
|
||||
installHint: "一行命令安装(Linux / macOS / Windows,内嵌 Node 运行时,解压即用)",
|
||||
installLabelPosix: "Linux / macOS",
|
||||
installLabelWindows: "Windows",
|
||||
installHint: "一行命令安装(内嵌 Node 运行时,解压即用)",
|
||||
stats: [
|
||||
{ value: "1000+", label: "支持模型数量" },
|
||||
{ value: "1×CPU", label: "最低运行配置" },
|
||||
@@ -74,6 +69,23 @@ export const zh = {
|
||||
],
|
||||
},
|
||||
|
||||
/** Install-method switcher (hero + quick start): OS tabs, online/offline methods. */
|
||||
install: {
|
||||
linux: "Linux",
|
||||
macos: "macOS",
|
||||
windows: "Windows",
|
||||
online: "在线安装",
|
||||
offline: "离线安装包",
|
||||
offlineNote:
|
||||
"每个 GitHub Release 附带五个自包含离线安装包(Linux / macOS 各 x64 与 arm64,Windows 为 x64),内含程序包、SHA256 校验文件与安装器:在有网机器下载,拷贝到目标机器安装,全程无需联网。",
|
||||
offlineHints: {
|
||||
linux: "arm64 机器换用 penguin-linux-arm64-offline.tar.gz。",
|
||||
macos: "Apple 芯片用 arm64 包,Intel 芯片换用 penguin-darwin-x64-offline.tar.gz。",
|
||||
windows: "解压后也可直接双击 install.cmd 完成安装。",
|
||||
},
|
||||
offlineRelease: "前往 GitHub Releases 下载离线安装包",
|
||||
},
|
||||
|
||||
copy: {
|
||||
copy: "复制",
|
||||
copied: "已复制",
|
||||
@@ -108,7 +120,7 @@ export const zh = {
|
||||
|
||||
compare: {
|
||||
eyebrow: "对比 LangChain",
|
||||
title: "1× 与 100× 的差距",
|
||||
title: "用 Agent 构建 Agent",
|
||||
subtitle: [
|
||||
"使用 LangChain,以 1 倍速度人工构建 Agent;",
|
||||
"使用 PenguinHarness,以 100 倍速度用 Agent 构建 Agent。",
|
||||
@@ -158,9 +170,8 @@ export const zh = {
|
||||
subtitle:
|
||||
"一行命令安装,打开桌面级界面即可让 Agent 开始工作;数据全部保存在本地 ~/.penguin/data 目录。",
|
||||
step1: "安装",
|
||||
step1Desc: "Linux / macOS / Windows,产物内嵌 Node 运行时,解压即用;升级与重装不触碰数据。",
|
||||
installLabelPosix: "Linux / macOS",
|
||||
installLabelWindows: "Windows(PowerShell)",
|
||||
step1Desc:
|
||||
"选择你的系统与安装方式:在线一行命令,或离线安装包;产物内嵌 Node 运行时,解压即用,升级与重装不触碰数据。",
|
||||
tabWeb: "Web 界面",
|
||||
tabCli: "命令行",
|
||||
webStep2: "启动 Web 界面",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Hero: enlarged logo + product name, the slogan inside the pill badge, then the
|
||||
* one-line headline whose rotating word crossfades through a gaussian blur
|
||||
* (Desktop <-> Server, localized per dictionary), three keywords, the install one-liner
|
||||
* and stats. The rotating word is a stacked inline-grid so line width never jumps.
|
||||
* Hero: enlarged logo + product name, the one-line headline whose rotating word
|
||||
* crossfades through a gaussian blur (Desktop <-> Server, localized per dictionary),
|
||||
* the one-sentence subtitle, the install one-liner behind an OS switcher, and stats.
|
||||
* The rotating word is a stacked inline-grid so line width never jumps.
|
||||
*/
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { S } from "../lib/strings";
|
||||
import { INSTALL_CMD, INSTALL_CMD_WINDOWS, REPO_URL } from "../lib/links";
|
||||
@@ -13,6 +13,54 @@ import { ArrowRightIcon, GitHubIcon } from "../components/icons";
|
||||
|
||||
const ROTATE_MS = 2600;
|
||||
|
||||
type InstallOs = "linux" | "macos" | "windows";
|
||||
|
||||
/**
|
||||
* Install one-liner behind an OS tab row: one command visible at a time (Linux and
|
||||
* macOS share the POSIX one-liner; the tabs still name them apart so nobody has to
|
||||
* know that). Prompt char follows the shell: $ for POSIX, > for PowerShell.
|
||||
*/
|
||||
function InstallBox() {
|
||||
const [os, setOs] = useState<InstallOs>("linux");
|
||||
const cmd = os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD;
|
||||
const osBtn = (active: boolean) =>
|
||||
`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100"
|
||||
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
}`;
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 text-left dark:border-gray-800 dark:bg-gray-900">
|
||||
<div
|
||||
className="flex items-center gap-1 border-b border-gray-200 bg-gray-100/70 px-2 py-1.5 dark:border-gray-800 dark:bg-gray-800/40"
|
||||
role="tablist"
|
||||
>
|
||||
{(["linux", "macos", "windows"] as const).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={os === key}
|
||||
className={osBtn(os === key)}
|
||||
onClick={() => setOs(key)}
|
||||
>
|
||||
{S.install[key]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 py-2.5 pr-2.5 pl-4">
|
||||
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
|
||||
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">
|
||||
{os === "windows" ? ">" : "$"}
|
||||
</span>
|
||||
{cmd}
|
||||
</code>
|
||||
<CopyButton text={cmd} className="shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RotatingWord({ words }: { words: string[] }) {
|
||||
const [active, setActive] = useState(0);
|
||||
useEffect(() => {
|
||||
@@ -50,16 +98,9 @@ export function Hero() {
|
||||
/>
|
||||
<span className="text-3xl font-semibold tracking-tight sm:text-4xl">{S.siteName}</span>
|
||||
</div>
|
||||
<p
|
||||
className="anim-rise mx-auto mt-6 inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-3.5 py-1 text-[13px] text-gray-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400"
|
||||
style={{ animationDelay: "40ms" }}
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-brand-500" aria-hidden="true" />
|
||||
{S.hero.badge}
|
||||
</p>
|
||||
{/* No text-balance: balance may break inside the breakable prefix ("…Agent /
|
||||
Builder…"); greedy wrapping + the nowrap span pins the desktop break to
|
||||
"Your Automated Agent Builder / Lives on Your Desktop". */}
|
||||
"Your Automated Agent Builder, / Right on Your Desktop". */}
|
||||
<h1
|
||||
className="anim-rise mx-auto mt-6 max-w-full text-3xl font-semibold tracking-tight sm:text-5xl"
|
||||
style={{ animationDelay: "80ms" }}
|
||||
@@ -72,22 +113,12 @@ export function Hero() {
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className="anim-rise mt-6 flex items-center justify-center gap-4 text-base font-medium text-gray-600 sm:text-lg dark:text-gray-300"
|
||||
<p
|
||||
className="anim-rise mt-6 text-base font-medium text-gray-600 sm:text-lg dark:text-gray-300"
|
||||
style={{ animationDelay: "140ms" }}
|
||||
>
|
||||
{S.hero.keywords.map((keyword, i) => (
|
||||
<Fragment key={keyword}>
|
||||
{i > 0 && (
|
||||
<span
|
||||
className="h-1 w-1 rounded-full bg-gray-300 dark:bg-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span>{keyword}</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{S.hero.subtitle}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="anim-rise mt-8 flex flex-wrap items-center justify-center gap-3"
|
||||
@@ -115,29 +146,7 @@ export function Hero() {
|
||||
className="anim-rise mx-auto mt-10 w-fit max-w-full"
|
||||
style={{ animationDelay: "240ms" }}
|
||||
>
|
||||
{/* One install row per OS (labels are language-neutral; prompt chars $ vs > match the shells). */}
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 text-left dark:border-gray-800 dark:bg-gray-900">
|
||||
<div className="flex items-center gap-3 py-2.5 pr-2.5 pl-4">
|
||||
<span className="w-24 shrink-0 text-[11px] font-medium text-gray-400 dark:text-gray-500">
|
||||
{S.hero.installLabelPosix}
|
||||
</span>
|
||||
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
|
||||
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">$</span>
|
||||
{INSTALL_CMD}
|
||||
</code>
|
||||
<CopyButton text={INSTALL_CMD} className="shrink-0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 border-t border-gray-200 py-2.5 pr-2.5 pl-4 dark:border-gray-800">
|
||||
<span className="w-24 shrink-0 text-[11px] font-medium text-gray-400 dark:text-gray-500">
|
||||
{S.hero.installLabelWindows}
|
||||
</span>
|
||||
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
|
||||
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">></span>
|
||||
{INSTALL_CMD_WINDOWS}
|
||||
</code>
|
||||
<CopyButton text={INSTALL_CMD_WINDOWS} className="shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
<InstallBox />
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">{S.hero.installHint}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
DEEPSEEK_KEYS_URL,
|
||||
INSTALL_CMD,
|
||||
INSTALL_CMD_WINDOWS,
|
||||
OFFLINE_INSTALL_CMDS,
|
||||
OPENROUTER_KEYS_URL,
|
||||
RELEASES_URL,
|
||||
} from "../lib/links";
|
||||
import { Section } from "../components/section";
|
||||
import { CodeCard } from "../components/code-card";
|
||||
@@ -75,6 +77,8 @@ function KeyLinks() {
|
||||
export function Quickstart() {
|
||||
const [mode, setMode] = useState<"web" | "cli">("web");
|
||||
const [provider, setProvider] = useState<"deepseek" | "openrouter">("deepseek");
|
||||
const [os, setOs] = useState<"linux" | "macos" | "windows">("linux");
|
||||
const [method, setMethod] = useState<"online" | "offline">("online");
|
||||
|
||||
const modeBtn = (active: boolean) =>
|
||||
`inline-flex items-center gap-1.5 rounded-md px-3.5 py-1.5 text-sm font-medium transition-colors ${
|
||||
@@ -130,12 +134,71 @@ export function Quickstart() {
|
||||
title={S.quickstart.step1}
|
||||
desc={S.quickstart.step1Desc}
|
||||
>
|
||||
<CodeCard code={INSTALL_CMD} label={S.quickstart.installLabelPosix} />
|
||||
<CodeCard
|
||||
code={INSTALL_CMD_WINDOWS}
|
||||
label={S.quickstart.installLabelWindows}
|
||||
className="mt-2"
|
||||
/>
|
||||
{/* Two switch rows — OS, then online/offline — so exactly one method shows at a time. */}
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
<div
|
||||
className="inline-flex gap-1 rounded-lg border border-gray-200 bg-white p-1 dark:border-gray-800 dark:bg-gray-900"
|
||||
role="tablist"
|
||||
>
|
||||
{(["linux", "macos", "windows"] as const).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={os === key}
|
||||
className={providerBtn(os === key)}
|
||||
onClick={() => setOs(key)}
|
||||
>
|
||||
{S.install[key]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="inline-flex gap-1 rounded-lg border border-gray-200 bg-white p-1 dark:border-gray-800 dark:bg-gray-900"
|
||||
role="tablist"
|
||||
>
|
||||
{(["online", "offline"] as const).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={method === key}
|
||||
className={providerBtn(method === key)}
|
||||
onClick={() => setMethod(key)}
|
||||
>
|
||||
{S.install[key]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{method === "online" ? (
|
||||
<CodeCard
|
||||
code={os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD}
|
||||
label={os === "windows" ? "PowerShell" : "curl | sh"}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{S.install.offlineNote}
|
||||
</p>
|
||||
<CodeCard
|
||||
code={OFFLINE_INSTALL_CMDS[os]}
|
||||
label={os === "windows" ? "PowerShell" : "shell"}
|
||||
/>
|
||||
<p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{S.install.offlineHints[os]}{" "}
|
||||
<a
|
||||
href={RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 text-brand-700 underline decoration-brand-300 underline-offset-2 transition-colors hover:text-brand-600 dark:text-brand-300 dark:decoration-brand-700"
|
||||
>
|
||||
{S.install.offlineRelease}
|
||||
<ExternalLinkIcon className="h-3 w-3" />
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Step>
|
||||
|
||||
{mode === "web" ? (
|
||||
|
||||
Reference in new issue
Block a user