docs(readme,landing): clearer install guidance with first-launch unsigned-build FAQ (#212)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-08-05 12:05:39 +08:00
1 parent a78901d70e
commit c6fb255a3b
6 files changed
+243 -30

No files matched your search

+17 -4
View File
@@ -102,6 +102,12 @@ export interface DesktopInstaller {
variant: string;
}
/** Named separately: the first-launch FAQ's chmod command quotes this exact file name. */
const LINUX_APPIMAGE: DesktopInstaller = {
file: "penguin-desktop-linux-x86_64.AppImage",
variant: "AppImage",
};
/** Installers per platform card, in display order. */
export const DESKTOP_INSTALLERS: Record<"mac" | "windows" | "linux", DesktopInstaller[]> = {
mac: [
@@ -109,11 +115,18 @@ export const DESKTOP_INSTALLERS: Record<"mac" | "windows" | "linux", DesktopInst
{ file: "penguin-desktop-darwin-x64.dmg", variant: "Intel" },
],
windows: [{ file: "penguin-desktop-win32-x64.exe", variant: "x64" }],
linux: [
{ file: "penguin-desktop-linux-x86_64.AppImage", variant: "AppImage" },
{ file: "penguin-desktop-linux-amd64.deb", variant: "deb" },
],
linux: [LINUX_APPIMAGE, { file: "penguin-desktop-linux-amd64.deb", variant: "deb" }],
};
/** Checksum list covering every desktop installer of a release. */
export const DESKTOP_SHA256SUMS = "SHA256SUMS.desktop";
/**
* First-launch fixes for the unsigned desktop builds (the download page FAQ).
* Language-neutral, like the install commands above. The macOS one deletes the
* quarantine flag that makes Gatekeeper report the app as "damaged"; the Linux
* one restores the execute bit browsers strip from a downloaded AppImage.
*/
export const MAC_UNQUARANTINE_CMD =
"sudo xattr -rd com.apple.quarantine /Applications/PenguinHarness.app";
export const LINUX_APPIMAGE_CHMOD_CMD = `chmod +x ${LINUX_APPIMAGE.file}`;
+25 -2
View File
@@ -105,8 +105,31 @@ export const en: Strings = {
altOss: "Use the OSS mirror instead",
checksums: "Checksums (SHA256SUMS.desktop)",
allReleases: "All releases",
unsignedNote:
'Current builds are unsigned: on first launch use right-click → Open on macOS, and "More info → Run anyway" past Windows SmartScreen.',
/** First-launch FAQ: one collapsible item per platform, the visitor's own pre-expanded. */
faq: {
title: "First-launch FAQ",
intro:
"Current builds are not signed yet, so the system may block the very first launch — the fix for your platform below is needed only once.",
mac: {
question: "macOS says “PenguinHarness” is damaged and can’t be opened?",
why: "macOS quarantines files downloaded from the internet, and the missing signature makes that flag surface as a false “damaged” alert. Deleting the flag clears it:",
stepDrag: "Open the downloaded dmg and drag PenguinHarness into the Applications folder.",
stepTerminal: "Open Terminal (Launchpad → Other → Terminal).",
stepPaste:
"Paste this command into Terminal and press Enter, then type your login password (nothing shows while you type; press Enter when done):",
stepOpen: "Once it finishes, double-click the app — it now opens normally.",
},
windows: {
question: "Windows SmartScreen says “Windows protected your PC”?",
answer:
"The installer is not signed yet, so SmartScreen holds the first run: click “More info”, then “Run anyway” to continue installing — first run only.",
},
linux: {
question: "Nothing happens when double-clicking the AppImage on Linux?",
answer:
"Browsers download AppImages without the execute permission. Grant it once and the app starts normally from then on (the deb package installs through the package manager and is not affected):",
},
},
cliHint: "Just need the CLI, or the Web UI in a browser?",
cliHintLink: "See the quick start",
},
+24 -2
View File
@@ -107,8 +107,30 @@ export const zh = {
altOss: "改用 OSS 镜像下载",
checksums: "校验和(SHA256SUMS.desktop)",
allReleases: "全部版本",
unsignedNote:
"当前构建暂未签名:macOS 首次启动请右键 →「打开」;Windows 在 SmartScreen 提示中选「更多信息 → 仍要运行」。",
/** First-launch FAQ: one collapsible item per platform, the visitor's own pre-expanded. */
faq: {
title: "首次启动常见问题",
intro: "当前构建暂未签名,系统可能拦截首次启动——按对应系统的步骤解除即可,只需操作一次。",
mac: {
question: "macOS 提示「PenguinHarness」已损坏,无法打开?",
why: "macOS 会给从网络下载的文件加上隔离标记,应用未签名时会因此被误报「已损坏」。删除该标记即可解除:",
stepDrag: "打开下载的 dmg,把 PenguinHarness 拖入「应用程序(Applications)」文件夹。",
stepTerminal: "打开终端:「启动台 → 其他 → 终端」。",
stepPaste:
"在终端粘贴这条命令并回车,然后输入开机密码(输入时屏幕不显示字符,输完回车即可):",
stepOpen: "执行完成后,双击即可正常打开应用。",
},
windows: {
question: "Windows SmartScreen 提示「Windows 已保护你的电脑」?",
answer:
"安装程序暂未签名,SmartScreen 会拦截首次运行:点「更多信息」,再点「仍要运行」即可继续安装,仅首次运行需要。",
},
linux: {
question: "Linux 双击 AppImage 没有反应?",
answer:
"浏览器下载的 AppImage 默认没有执行权限,赋权一次后即可正常启动(deb 包经包管理器安装,无此问题):",
},
},
cliHint: "只需要命令行或浏览器里的 Web 界面?",
cliHintLink: "参见快速开始",
},
+71 -4
View File
@@ -5,20 +5,27 @@
* resolves (validated the same way the installers validate it; any failure, e.g. CORS
* not configured on the bucket or the mirror unreachable, silently keeps the GitHub
* links). Plain-anchor downloads are never CORS-gated — only this version lookup is.
* Below the cards, a first-launch FAQ covers the unsigned builds — one collapsible item
* per platform (macOS quarantine removal, SmartScreen, AppImage execute bit), with the
* visitor's own platform pre-expanded.
*/
import { useEffect, useState } from "react";
import { Link } from "react-router";
import type { ReactNode } from "react";
import { S } from "../lib/strings";
import {
DESKTOP_INSTALLERS,
DESKTOP_SHA256SUMS,
GITHUB_LATEST_DOWNLOAD,
LINUX_APPIMAGE_CHMOD_CMD,
MAC_UNQUARANTINE_CMD,
OSS_LATEST_JSON_URL,
OSS_ORIGIN,
RELEASES_URL,
} from "../lib/links";
import { Section } from "../components/section";
import { DownloadIcon, ExternalLinkIcon } from "../components/icons";
import { CodeCard } from "../components/code-card";
import { ChevronDownIcon, DownloadIcon, ExternalLinkIcon } from "../components/icons";
type Platform = "mac" | "windows" | "linux";
const PLATFORMS: Platform[] = ["mac", "windows", "linux"];
@@ -37,6 +44,36 @@ interface Mirror {
base: string;
}
/**
* One collapsible item of the first-launch FAQ. `defaultOpen` pre-expands the visitor's
* own platform on mount; after that the element owns its open state (React only writes
* the `open` property again if the rendered value changes, which it never does here).
*/
function FaqItem({
question,
defaultOpen,
children,
}: {
question: string;
defaultOpen: boolean;
children: ReactNode;
}) {
return (
<details
open={defaultOpen}
className="group rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
>
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 text-sm font-medium tracking-tight [&::-webkit-details-marker]:hidden">
{question}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-gray-400 transition-transform group-open:rotate-180 dark:text-gray-500" />
</summary>
<div className="border-t border-gray-200 px-4 py-3 text-sm leading-6 text-gray-600 dark:border-gray-800 dark:text-gray-400">
{children}
</div>
</details>
);
}
/** latest.json validated like the installer forwarders validate it: schema 1, safe v-tag, fixed bucket base. */
function parseMirror(value: unknown): Mirror | null {
if (typeof value !== "object" || value === null) return null;
@@ -135,10 +172,40 @@ export function DownloadPage() {
<ExternalLinkIcon className="h-3 w-3" />
</a>
</p>
<p className="mt-6 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.download.unsignedNote}
</div>
<div className="mx-auto mt-12 max-w-2xl">
<h3 className="text-center text-base font-semibold tracking-tight">
{S.download.faq.title}
</h3>
<p className="mt-1 text-center text-sm leading-6 text-gray-600 dark:text-gray-400">
{S.download.faq.intro}
</p>
<p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
<div className="mt-4 flex flex-col gap-3 text-left">
<FaqItem question={S.download.faq.mac.question} defaultOpen={detected === "mac"}>
<p>{S.download.faq.mac.why}</p>
<ol className="mt-2 flex list-decimal flex-col gap-1.5 pl-5">
<li>{S.download.faq.mac.stepDrag}</li>
<li>{S.download.faq.mac.stepTerminal}</li>
<li>
{S.download.faq.mac.stepPaste}
<CodeCard code={MAC_UNQUARANTINE_CMD} label="Terminal" className="mt-2 mb-1" />
</li>
<li>{S.download.faq.mac.stepOpen}</li>
</ol>
</FaqItem>
<FaqItem question={S.download.faq.windows.question} defaultOpen={detected === "windows"}>
<p>{S.download.faq.windows.answer}</p>
</FaqItem>
<FaqItem question={S.download.faq.linux.question} defaultOpen={detected === "linux"}>
<p>{S.download.faq.linux.answer}</p>
<CodeCard code={LINUX_APPIMAGE_CHMOD_CMD} label="shell" className="mt-2" />
</FaqItem>
</div>
</div>
<div className="mx-auto mt-10 max-w-4xl text-center">
<p className="text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.download.cliHint}{" "}
<Link to="/#quickstart" className={textLink}>
{S.download.cliHintLink}