docs(readme,landing): introduce the desktop app (#201)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-05 00:21:15 +08:00
committed by GitHub
parent e0f6b122c8
commit 1cf0e4d9bc
21 changed files with 342 additions and 17 deletions
+5 -5
View File
@@ -75,8 +75,8 @@ jobs:
name: desktop-${{ runner.os }}
if-no-files-found: error
path: |
packages/desktop/stage/out/*.AppImage
packages/desktop/stage/out/*.deb
packages/desktop/stage/out/*.dmg
packages/desktop/stage/out/*-mac.zip
packages/desktop/stage/out/*.exe
packages/desktop/stage/out/penguin-desktop-*.AppImage
packages/desktop/stage/out/penguin-desktop-*.deb
packages/desktop/stage/out/penguin-desktop-*.dmg
packages/desktop/stage/out/penguin-desktop-*.zip
packages/desktop/stage/out/penguin-desktop-*.exe
+2 -2
View File
@@ -316,8 +316,8 @@ jobs:
# Desktop installers built by the desktop job (three-OS matrix): collected here so
# they are part of the Release's initial (immutable) asset set. Their checksums go
# in a separate SHA256SUMS.desktop — the OSS mirror script's canonical file list
# stays untouched (desktop installers are not mirrored yet).
# in a separate SHA256SUMS.desktop, which is also how the OSS mirror script verifies
# them (its manifest lists the installers by their version-less names).
- name: Collect desktop artifacts
uses: actions/download-artifact@v4
with:
+5 -1
View File
@@ -138,6 +138,10 @@ npm install -g @prismshadow/penguin-cli
penguin web # start the service and open http://127.0.0.1:7364
```
### 🖥️ Desktop app
The full Web experience as a standalone application: it embeds the server and opens already signed in — no terminal, no login page, no initial password to copy — and it works on the same `~/.penguin/data` root as a CLI install, so the two can be used interchangeably (a data root only ever runs one server; if a CLI-started instance is already up, the app attaches to it). Installers for macOS (dmg, Apple silicon / Intel), Windows and Linux (AppImage / deb) live on the [download page](https://penguin.ooo/download) — which serves the OSS-accelerated mirror when it is reachable — and are attached to each [GitHub Release](https://github.com/Prism-Shadow/penguin-harness/releases). Current builds are unsigned: on first launch use right-click → Open on macOS, and "More info → Run anyway" past Windows SmartScreen.
<details>
<summary><b>📴 Offline install (air-gapped machines)</b></summary>
@@ -198,7 +202,7 @@ for await (const output of session.run([userText("Create hello.txt containing hi
## Roadmap
- [ ] Public release of the benchmark suite
- [ ] Desktop app
- [x] Desktop app
- [x] Windows support
- [ ] Agent company and templates
- [ ] Company-level self evolving
+5 -1
View File
@@ -138,6 +138,10 @@ npm install -g @prismshadow/penguin-cli
penguin web # 启动服务并打开 http://127.0.0.1:7364
```
### 🖥️ 桌面端应用
完整的 Web 体验打包为独立应用:内嵌服务端,打开即已登录——无需终端、无登录页、也不用抄初始密码——并与 CLI 安装共用同一个 `~/.penguin/data` 数据目录,两者可以混用(一个数据目录同一时刻只运行一个服务端;CLI 已启动实例时,应用会直接接入它)。macOS(dmg,Apple 芯片 / Intel)、Windows 与 Linux(AppImage / deb)的安装包可在[下载页](https://penguin.ooo/download)获取——国内自动走 OSS 镜像加速——也附于每个 [GitHub Release](https://github.com/Prism-Shadow/penguin-harness/releases)。当前构建暂未签名:macOS 首次启动请右键 →「打开」,Windows 在 SmartScreen 提示中选「仍要运行」。
<details>
<summary><b>📴 离线安装(无网环境)</b></summary>
@@ -198,7 +202,7 @@ for await (const output of session.run([userText("Create hello.txt containing hi
## 路线图
- [ ] Benchmark 套件正式发布
- [ ] 桌面端应用
- [x] 桌面端应用
- [x] Windows 系统支持
- [ ] Agent 公司与模板
- [ ] 公司级自进化能力
+7
View File
@@ -26,6 +26,11 @@ mac:
category: public.app-category.developer-tools
# No Developer ID yet (M4): identity null skips codesign instead of failing.
identity: null
# Version-less artifact names, following the CLI bundles (penguin-darwin-arm64.tar.gz):
# the version lives in the Release tag and SHA256SUMS.desktop, so the download page and
# the OSS mirror manifest can address every installer by a stable name — and GitHub's
# `releases/latest/download/<name>` stays a working static link across releases.
artifactName: penguin-desktop-darwin-${arch}.${ext}
target:
- target: dmg
arch: [arm64, x64]
@@ -33,6 +38,7 @@ mac:
arch: [arm64, x64]
win:
artifactName: penguin-desktop-win32-${arch}.${ext}
target:
- target: nsis
arch: [x64]
@@ -50,6 +56,7 @@ nsis:
linux:
# The scoped package name is not a valid executable file name.
executableName: penguin-harness
artifactName: penguin-desktop-linux-${arch}.${ext}
category: Development
maintainer: Prism Shadow <zheng@prismshadow.com>
target:
+3
View File
@@ -104,6 +104,9 @@ export function Nav({
<a href={`${SITE_URL}blog`} className={deskLinkCls(false)} onMouseEnter={slideTo}>
{S.nav.blog}
</a>
<a href={`${SITE_URL}download`} className={deskLinkCls(false)} onMouseEnter={slideTo}>
{S.nav.download}
</a>
{/* "Docs" is this SPA's own root — a router Link, and always the current page. */}
<Link to="/" className={deskLinkCls(true)} onMouseEnter={slideTo} aria-current="page">
{S.nav.docs}
+1
View File
@@ -13,6 +13,7 @@ export const en: Strings = {
contract: "CONTRACT.md",
features: "Features",
blog: "Blog",
download: "Download",
docs: "Docs",
github: "GitHub",
openMenu: "Open navigation",
+1
View File
@@ -19,6 +19,7 @@ export const zh = {
contract: "CONTRACT.md",
features: "功能",
blog: "博客",
download: "下载",
docs: "文档",
github: "GitHub",
openMenu: "打开目录",
+3 -5
View File
@@ -16,7 +16,7 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { absoluteUrl, blogRoutes } from "./site-routes.mjs";
import { absoluteUrl, blogRoutes, pageRoutes } from "./site-routes.mjs";
const dist = join(dirname(fileURLToPath(import.meta.url)), "..", "dist");
const shell = readFileSync(join(dist, "index.html"), "utf8");
@@ -43,7 +43,7 @@ function shellFor(route) {
.replace(OG_URL, `<meta property="og:url" content="${url}" />`);
}
const routes = blogRoutes();
const routes = [...pageRoutes(), ...blogRoutes()];
for (const { route } of routes) {
const dir = join(dist, route);
mkdirSync(dir, { recursive: true });
@@ -52,6 +52,4 @@ for (const { route } of routes) {
writeFileSync(join(dist, "404.html"), shell.replace(CANONICAL, ""));
writeFileSync(join(dist, ".nojekyll"), "");
console.log(
`[postbuild] wrote ${routes.length} blog route shells, dist/404.html and dist/.nojekyll`,
);
console.log(`[postbuild] wrote ${routes.length} route shells, dist/404.html and dist/.nojekyll`);
+7
View File
@@ -59,6 +59,13 @@ function newestDate(dir, slug) {
* that actually answers 200 — the one canonical tags and the sitemap should name.
* The home page is excluded; Vite's own index.html already sits at the dist root.
*/
/** Non-blog landing SPA pages (currently the desktop download page). */
export function pageRoutes() {
return [{ route: "/download/" }];
}
/** Blog routes, one per post plus the list page. */
export function blogRoutes() {
return [
{ route: "/blog/" },
@@ -37,6 +37,11 @@ export function Footer() {
{S.footer.blog}
</Link>
</li>
<li>
<Link to="/download" className="hover:text-gray-900 dark:hover:text-gray-100">
{S.nav.download}
</Link>
</li>
</ul>
</div>
<div>
+16
View File
@@ -108,6 +108,14 @@ export function Nav() {
>
{S.nav.blog}
</Link>
<Link
to="/download"
className={deskLinkCls(activeItem === "download")}
onMouseEnter={slideTo}
aria-current={activeItem === "download" ? "page" : undefined}
>
{S.nav.download}
</Link>
{/* Docs is a sibling SPA under /docs/ — a plain anchor, not a router Link. */}
<a href={DOCS_URL} className={deskLinkCls(false)} onMouseEnter={slideTo}>
{S.nav.docs}
@@ -139,6 +147,14 @@ export function Nav() {
>
{S.nav.blog}
</Link>
<Link
to="/download"
className={mobileLinkCls(activeItem === "download")}
onClick={() => setOpen(false)}
aria-current={activeItem === "download" ? "page" : undefined}
>
{S.nav.download}
</Link>
<a href={DOCS_URL} className={mobileLinkCls(false)} onClick={() => setOpen(false)}>
{S.nav.docs}
</a>
+37
View File
@@ -80,3 +80,40 @@ export const blogAssetUrl = (name: string): string => `${COMMUNITY_RAW}/blog-ass
/** API key consoles (same URLs the in-app Models page links to). */
export const DEEPSEEK_KEYS_URL = "https://platform.deepseek.com/api_keys";
export const OPENROUTER_KEYS_URL = "https://openrouter.ai/workspaces/default/keys";
/**
* Desktop app downloads. The installers carry version-less names (see
* packages/desktop/electron-builder.yml), which is what makes static links possible on a
* Pages-hosted site: GitHub's `releases/latest/download/<name>` always serves the newest
* release, no client-side resolution needed. The OSS mirror stores immutable per-tag
* directories instead, so mirror links DO need the current tag — the download page reads
* it from the bucket's `latest.json` pointer (the same file install.sh resolves) and
* falls back to the GitHub links when that fetch fails (e.g. CORS not configured, or the
* mirror unreachable).
*/
export const OSS_ORIGIN = "https://penguin-harness-releases.oss-cn-beijing.aliyuncs.com";
export const OSS_LATEST_JSON_URL = `${OSS_ORIGIN}/latest.json`;
export const GITHUB_LATEST_DOWNLOAD = `${REPO_URL}/releases/latest/download`;
export interface DesktopInstaller {
/** Stable asset file name, identical on GitHub Releases and the OSS mirror. */
file: string;
/** Language-neutral variant tag rendered on the button (arch / package format). */
variant: string;
}
/** Installers per platform card, in display order. */
export const DESKTOP_INSTALLERS: Record<"mac" | "windows" | "linux", DesktopInstaller[]> = {
mac: [
{ file: "penguin-desktop-darwin-arm64.dmg", variant: "Apple Silicon" },
{ file: "penguin-desktop-darwin-x64.dmg", variant: "Intel" },
],
windows: [{ file: "penguin-desktop-win32-x64.exe", variant: "x64" }],
linux: [
{ file: "penguin-desktop-linux-x64.AppImage", variant: "AppImage" },
{ file: "penguin-desktop-linux-x64.deb", variant: "deb" },
],
};
/** Checksum list covering every desktop installer of a release. */
export const DESKTOP_SHA256SUMS = "SHA256SUMS.desktop";
+2 -1
View File
@@ -13,7 +13,7 @@ export const SECTION_IDS = [
] as const;
export type SectionId = (typeof SECTION_IDS)[number];
export type ActiveNavItem = SectionId | "blog" | null;
export type ActiveNavItem = SectionId | "blog" | "download" | null;
function isSectionId(value: string): value is SectionId {
return SECTION_IDS.some((id) => id === value);
@@ -21,6 +21,7 @@ function isSectionId(value: string): value is SectionId {
export function getActiveNavItem(pathname: string, hash: string): ActiveNavItem {
if (pathname === "/blog" || pathname.startsWith("/blog/")) return "blog";
if (pathname === "/download") return "download";
if (pathname !== "/") return null;
const id = hash.startsWith("#") ? hash.slice(1) : hash;
+31
View File
@@ -25,6 +25,7 @@ export const en: Strings = {
contract: "CONTRACT.md",
features: "Features",
blog: "Blog",
download: "Download",
docs: "Docs",
github: "GitHub",
openMenu: "Open menu",
@@ -78,6 +79,36 @@ export const en: Strings = {
windows: "After unzipping you can also just double-click install.cmd.",
},
offlineRelease: "Download offline packages from GitHub Releases",
desktopNote:
"Prefer a standalone app? The desktop app packages the Web interface and the server into a terminal-free installer (macOS / Windows / Linux), sharing the same local data as a CLI install.",
desktopPage: "Go to the desktop download page",
},
download: {
eyebrow: "Download",
title: "Download the desktop app",
subtitle:
"The full Web experience as a standalone application: it embeds the server and opens already signed in — no terminal, no login page; data lives in the same ~/.penguin/data root as a CLI install.",
recommended: "Your system",
platforms: {
mac: { name: "macOS", require: "macOS 11 or later, dmg disk image (pick your chip)" },
windows: { name: "Windows", require: "Windows 10 or later (x64), NSIS installer" },
linux: {
name: "Linux",
require: "x64 — AppImage runs in place, deb goes to your package manager",
},
},
statusOss: (version: string) =>
`Connected to the OSS mirror (${version}) — downloads are served from the mirror.`,
statusGithub: "Downloads point at the latest GitHub Release.",
altGithub: "Download from GitHub instead",
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.',
cliHint: "Just need the CLI, or the Web UI in a browser?",
cliHintLink: "See the quick start",
},
copy: {
+27
View File
@@ -26,6 +26,7 @@ export const zh = {
contract: "CONTRACT.md",
features: "功能",
blog: "博客",
download: "下载",
docs: "文档",
github: "GitHub",
openMenu: "打开菜单",
@@ -84,6 +85,32 @@ export const zh = {
windows: "解压后也可直接双击 install.cmd 完成安装。",
},
offlineRelease: "前往 GitHub Releases 下载离线安装包",
desktopNote:
"更喜欢独立应用?桌面端把 Web 界面与服务端打包成免终端的安装程序(macOS / Windows / Linux),与 CLI 安装共用同一份本地数据。",
desktopPage: "前往桌面端下载页",
},
download: {
eyebrow: "下载",
title: "下载桌面端",
subtitle:
"完整的 Web 体验打包为独立应用:内嵌服务端,打开即已登录——无需终端、无登录页;数据与 CLI 安装共用同一个 ~/.penguin/data 目录。",
recommended: "当前系统",
platforms: {
mac: { name: "macOS", require: "macOS 11 及以上,dmg 安装镜像(按芯片选择)" },
windows: { name: "Windows", require: "Windows 10 及以上(x64),NSIS 安装程序" },
linux: { name: "Linux", require: "x64,AppImage 免安装运行,或 deb 交给包管理器" },
},
statusOss: (version: string) => `已连接 OSS 国内镜像(${version}),点击即从镜像高速下载。`,
statusGithub: "下载指向 GitHub Releases 的最新版本。",
altGithub: "改从 GitHub 下载",
altOss: "改用 OSS 镜像下载",
checksums: "校验和(SHA256SUMS.desktop)",
allReleases: "全部版本",
unsignedNote:
"当前构建暂未签名:macOS 首次启动请右键 →「打开」;Windows 在 SmartScreen 提示中选「更多信息 → 仍要运行」。",
cliHint: "只需要命令行或浏览器里的 Web 界面?",
cliHintLink: "参见快速开始",
},
copy: {
+150
View File
@@ -0,0 +1,150 @@
/**
* Desktop download page. The buttons are static GitHub `releases/latest/download/<name>`
* links by default — always valid because installer names are version-less — and swap to
* the OSS mirror's immutable per-tag URLs once the bucket's `latest.json` pointer
* 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.
*/
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { S } from "../lib/strings";
import {
DESKTOP_INSTALLERS,
DESKTOP_SHA256SUMS,
GITHUB_LATEST_DOWNLOAD,
OSS_LATEST_JSON_URL,
OSS_ORIGIN,
RELEASES_URL,
} from "../lib/links";
import { Section } from "../components/section";
import { DownloadIcon, ExternalLinkIcon } from "../components/icons";
type Platform = "mac" | "windows" | "linux";
const PLATFORMS: Platform[] = ["mac", "windows", "linux"];
/** Coarse OS detection, used only to badge one card; absent or wrong detection changes nothing else. */
function detectPlatform(): Platform | null {
const ua = navigator.userAgent;
if (/Windows/i.test(ua)) return "windows";
if (/Macintosh|Mac OS X/i.test(ua)) return "mac";
if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux";
return null;
}
interface Mirror {
tag: string;
base: string;
}
/** 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;
const manifest = value as { schemaVersion?: unknown; tag?: unknown; releaseBaseUrl?: unknown };
if (manifest.schemaVersion !== 1 || typeof manifest.tag !== "string") return null;
if (!/^v[0-9A-Za-z][0-9A-Za-z._-]*$/.test(manifest.tag)) return null;
if (manifest.releaseBaseUrl !== `${OSS_ORIGIN}/releases/${manifest.tag}`) return null;
return { tag: manifest.tag, base: manifest.releaseBaseUrl };
}
export function DownloadPage() {
const [mirror, setMirror] = useState<Mirror | null>(null);
const [forceGithub, setForceGithub] = useState(false);
useEffect(() => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
fetch(OSS_LATEST_JSON_URL, { signal: controller.signal })
.then((res) => (res.ok ? res.json() : null))
.then((body: unknown) => {
const parsed = parseMirror(body);
if (parsed) setMirror(parsed);
})
.catch(() => {}) // The GitHub links already work; the mirror is a progressive upgrade.
.finally(() => clearTimeout(timer));
return () => {
clearTimeout(timer);
controller.abort();
};
}, []);
const detected = detectPlatform();
const viaMirror = mirror !== null && !forceGithub;
const hrefFor = (file: string) =>
viaMirror ? `${mirror.base}/${file}` : `${GITHUB_LATEST_DOWNLOAD}/${file}`;
const textLink =
"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";
return (
<Section eyebrow={S.download.eyebrow} title={S.download.title} subtitle={S.download.subtitle}>
<div className="mx-auto grid max-w-4xl gap-4 sm:grid-cols-3">
{PLATFORMS.map((platform) => (
<div
key={platform}
className={`flex flex-col rounded-xl border bg-white p-5 dark:bg-gray-900 ${
detected === platform
? "border-brand-500 ring-1 ring-brand-500"
: "border-gray-200 dark:border-gray-800"
}`}
>
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold tracking-tight">
{S.download.platforms[platform].name}
</h3>
{detected === platform && (
<span className="rounded-full bg-brand-600/10 px-2 py-0.5 text-xs font-medium text-brand-700 dark:text-brand-300">
{S.download.recommended}
</span>
)}
</div>
<p className="mt-1 mb-4 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.download.platforms[platform].require}
</p>
<div className="mt-auto flex flex-col gap-2">
{DESKTOP_INSTALLERS[platform].map(({ file, variant }) => (
<a
key={file}
href={hrefFor(file)}
className="inline-flex items-center justify-center gap-1.5 rounded-md bg-gray-900 px-3.5 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-700 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-300"
>
<DownloadIcon className="h-4 w-4" />
{variant}
</a>
))}
</div>
</div>
))}
</div>
<div className="mx-auto mt-8 max-w-4xl text-center text-sm text-gray-600 dark:text-gray-400">
<p>
{viaMirror ? S.download.statusOss(mirror.tag) : S.download.statusGithub}{" "}
{mirror !== null && (
<button type="button" className={textLink} onClick={() => setForceGithub(!forceGithub)}>
{forceGithub ? S.download.altOss : S.download.altGithub}
</button>
)}
</p>
<p className="mt-2">
<a href={hrefFor(DESKTOP_SHA256SUMS)} className={textLink}>
{S.download.checksums}
</a>
<span className="mx-2 text-gray-300 dark:text-gray-700">·</span>
<a href={RELEASES_URL} target="_blank" rel="noreferrer" className={textLink}>
{S.download.allReleases}
<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}
</p>
<p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.download.cliHint}{" "}
<Link to="/#quickstart" className={textLink}>
{S.download.cliHintLink}
</Link>
</p>
</div>
</Section>
);
}
+2
View File
@@ -13,6 +13,7 @@ import { NeonBackground } from "./components/neon-bg";
import { HomePage } from "./pages/home";
import { BlogListPage } from "./pages/blog-list";
import { BlogPostPage } from "./pages/blog-post";
import { DownloadPage } from "./pages/download";
/**
* Deep links into the docs SPA that missed a real file (e.g. an unknown slug) land on
@@ -76,6 +77,7 @@ export function AppRouter() {
<Route index element={<HomePage />} />
<Route path="/blog" element={<BlogListPage />} />
<Route path="/blog/:slug" element={<BlogPostPage />} />
<Route path="/download" element={<DownloadPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
{/* Outside Layout: a pure redirect, no nav/footer flash. */}
@@ -5,6 +5,7 @@
* the string dictionaries.
*/
import { useState } from "react";
import { Link } from "react-router";
import type { ReactNode } from "react";
import { S } from "../lib/strings";
import {
@@ -199,6 +200,17 @@ export function Quickstart() {
</p>
</>
)}
{/* Below both install methods: the desktop app replaces this whole flow for
users who never want a terminal, so it stays visible on every switch. */}
<p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.install.desktopNote}{" "}
<Link
to="/download"
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.desktopPage}
</Link>
</p>
</Step>
{mode === "web" ? (
+7 -2
View File
@@ -14,7 +14,12 @@ import { execSync } from "node:child_process";
import { cpSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { absoluteUrl, blogRoutes, docsRoutes } from "../packages/landing/scripts/site-routes.mjs";
import {
absoluteUrl,
blogRoutes,
docsRoutes,
pageRoutes,
} from "../packages/landing/scripts/site-routes.mjs";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const base = process.env.BASE_PATH ?? "/";
@@ -40,7 +45,7 @@ cpSync(join(root, "packages", "docs", "dist"), docsTarget, { recursive: true });
// that has to name routes from both sites. It is also the only way a crawler learns
// those routes — both pages ship an empty <div id="root"> and build their navigation
// in JS, so following links is not an option.
const routes = [{ route: "/" }, ...blogRoutes(), ...docsRoutes()];
const routes = [{ route: "/" }, ...pageRoutes(), ...blogRoutes(), ...docsRoutes()];
const sitemap = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
+14
View File
@@ -71,6 +71,17 @@ penguin-darwin-arm64.tar.gz
penguin-universal.tar.gz
penguin-win32-x64.zip
"
# Desktop installers carry version-less names (see packages/desktop/electron-builder.yml)
# and are verified as a set through SHA256SUMS.desktop instead of per-file .sha256 twins.
DESKTOP_INSTALLERS="
penguin-desktop-darwin-arm64.dmg
penguin-desktop-darwin-arm64.zip
penguin-desktop-darwin-x64.dmg
penguin-desktop-darwin-x64.zip
penguin-desktop-linux-x64.AppImage
penguin-desktop-linux-x64.deb
penguin-desktop-win32-x64.exe
"
FILES="$BUNDLES
penguin-linux-x64.tar.gz.sha256
penguin-linux-arm64.tar.gz.sha256
@@ -79,6 +90,8 @@ penguin-darwin-arm64.tar.gz.sha256
penguin-universal.tar.gz.sha256
penguin-win32-x64.zip.sha256
SHA256SUMS
$DESKTOP_INSTALLERS
SHA256SUMS.desktop
install.sh
install.ps1
"
@@ -94,6 +107,7 @@ for bundle in $BUNDLES; do
(cd "$RELEASE_DIR" && sha256sum -c "$bundle.sha256")
done
(cd "$RELEASE_DIR" && sha256sum -c SHA256SUMS)
(cd "$RELEASE_DIR" && sha256sum -c SHA256SUMS.desktop)
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT