diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx index 025429ec..6083fae1 100644 --- a/landing/app/layout.tsx +++ b/landing/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; import { ThemeProvider } from "../components/theme-provider"; +import { platformSupportSummary } from "../lib/platform-support"; const description = "Local-first desktop control plane for autonomous AI companies. Set a goal, let an AI CEO assemble specialists, delegate work, and deliver code, research, content, and operations from your desktop."; @@ -143,7 +144,7 @@ const faqJsonLd = { name: "How do I download AgentCompany?", acceptedAnswer: { "@type": "Answer", - text: "Use the download section on the website to choose the installer for your platform. macOS ships as a signed and notarized DMG. Windows and Linux builds are distributed directly from our Cloudflare release host.", + text: platformSupportSummary.downloadAnswer, }, }, { @@ -151,7 +152,7 @@ const faqJsonLd = { name: "What platforms does AgentCompany support?", acceptedAnswer: { "@type": "Answer", - text: "AgentCompany is available on macOS, Windows, and Linux. It is built with Electron and runs natively on all three platforms with full feature parity.", + text: platformSupportSummary.platformAnswer, }, }, { diff --git a/landing/components/faq.tsx b/landing/components/faq.tsx index 7dcaee0e..e7093b96 100644 --- a/landing/components/faq.tsx +++ b/landing/components/faq.tsx @@ -2,6 +2,7 @@ import { motion } from "framer-motion"; import { useState } from "react"; +import { platformSupportSummary } from "../lib/platform-support"; const faqs = [ { @@ -11,13 +12,11 @@ const faqs = [ }, { question: "How do I download AgentCompany?", - answer: - "Use the download section on this page to choose the build that matches your architecture and package format. macOS ships as signed and notarized DMGs, Windows ships as a universal installer covering ARM64, x64, and x86, and Linux ships as AppImage, DEB, and RPM builds from our Cloudflare release host.", + answer: platformSupportSummary.downloadAnswer, }, { question: "What platforms does AgentCompany support?", - answer: - "AgentCompany is available on macOS arm64 and x64, Windows ARM64/x64/x86, and Linux x64 and ARM64. It is built with Electron and the desktop feature set stays aligned across all supported targets.", + answer: platformSupportSummary.platformAnswer, }, { question: "What AI models can AgentCompany use?", diff --git a/landing/components/release-links.generated.ts b/landing/components/release-links.generated.ts index 2175d69f..7a2ee9b9 100644 --- a/landing/components/release-links.generated.ts +++ b/landing/components/release-links.generated.ts @@ -1,12 +1,16 @@ export type PlatformDownloadAsset = { + architectures: ReleaseArchitecture[]; id: string; label: string; href: string; note: string; }; +export type ReleaseArchitecture = "arm64" | "x64" | "x86"; + export type PlatformDownload = { id: "macos" | "windows" | "linux"; + packageDescription: string; title: string; note: string; assets: PlatformDownloadAsset[]; @@ -17,16 +21,19 @@ export const releaseVersion = "0.4.174"; export const platformDownloads: PlatformDownload[] = [ { id: "macos", + packageDescription: "signed and notarized DMG builds", title: "macOS", note: "Signed + notarized DMG builds", assets: [ { + architectures: ["arm64"], id: "macos-arm64-dmg", label: "Apple Silicon DMG", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174-arm64.dmg", note: "arm64", }, { + architectures: ["x64"], id: "macos-x64-dmg", label: "Intel Mac DMG", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174.dmg", @@ -36,10 +43,12 @@ export const platformDownloads: PlatformDownload[] = [ }, { id: "windows", + packageDescription: "an NSIS installer build", title: "Windows", note: "NSIS installer builds", assets: [ { + architectures: ["arm64", "x64"], id: "windows-universal-installer", label: "Windows Universal Installer", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.174.exe", @@ -49,40 +58,47 @@ export const platformDownloads: PlatformDownload[] = [ }, { id: "linux", + packageDescription: "AppImage, DEB, and RPM builds", title: "Linux", note: "AppImage, DEB, and RPM builds", assets: [ { + architectures: ["x64"], id: "linux-x64-appimage", label: "Linux x64 AppImage", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174.AppImage", note: "x64", }, { + architectures: ["x64"], id: "linux-x64-deb", label: "Linux x64 DEB", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.174_amd64.deb", note: "x64", }, { + architectures: ["x64"], id: "linux-x64-rpm", label: "Linux x64 RPM", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.174.x86_64.rpm", note: "x64", }, { + architectures: ["arm64"], id: "linux-arm64-appimage", label: "Linux ARM64 AppImage", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174-arm64.AppImage", note: "arm64", }, { + architectures: ["arm64"], id: "linux-arm64-deb", label: "Linux ARM64 DEB", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.174_arm64.deb", note: "arm64", }, { + architectures: ["arm64"], id: "linux-arm64-rpm", label: "Linux ARM64 RPM", href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.174.aarch64.rpm", diff --git a/landing/lib/platform-support.ts b/landing/lib/platform-support.ts new file mode 100644 index 00000000..0cb6b083 --- /dev/null +++ b/landing/lib/platform-support.ts @@ -0,0 +1,106 @@ +import { + platformDownloads, + type PlatformDownload, + type PlatformDownloadAsset, + type ReleaseArchitecture, +} from "../components/release-links.generated"; + +const ARCHITECTURE_ORDER: ReleaseArchitecture[] = ["arm64", "x64", "x86"]; + +export type PlatformSupportSummary = { + downloadAnswer: string; + platformAnswer: string; +}; + +export const platformSupportSummary = buildPlatformSupportSummary(platformDownloads); + +export function buildPlatformSupportSummary(downloads: PlatformDownload[]): PlatformSupportSummary { + const macos = requirePlatform(downloads, "macos"); + const windows = requirePlatform(downloads, "windows"); + const linux = requirePlatform(downloads, "linux"); + + return { + downloadAnswer: + `Use the download section on this page to choose the build that matches your architecture and package format. ` + + `${formatDownloadSentence(macos)} ${formatDownloadSentence(windows)} ${formatDownloadSentence(linux)}`, + platformAnswer: + `AgentCompany is available on ${formatAvailability(macos)}, ${formatAvailability(windows)}, ` + + `and ${formatAvailability(linux)}. It is built with Electron and the desktop feature set stays aligned across all supported targets.`, + }; +} + +function requirePlatform(downloads: PlatformDownload[], id: PlatformDownload["id"]): PlatformDownload { + const platform = downloads.find((download) => download.id === id); + if (!platform) { + throw new Error(`Missing ${id} release download metadata.`); + } + return platform; +} + +function formatDownloadSentence(download: PlatformDownload): string { + const architectures = formatArchitectureList(collectArchitectures(download)); + + if (!download.packageDescription.trim()) { + throw new Error(`Release platform ${download.id} must include package format copy.`); + } + + return `${download.title} ships as ${download.packageDescription} for ${architectures}.`; +} + +function formatAvailability(download: PlatformDownload): string { + return `${download.title} (${formatArchitectureList(collectArchitectures(download), "comma")})`; +} + +function collectArchitectures(download: PlatformDownload): ReleaseArchitecture[] { + const architectures = new Set(); + + for (const asset of download.assets) { + collectAssetArchitectures(download.id, asset).forEach((architecture) => architectures.add(architecture)); + } + + return [...architectures].sort((left, right) => architectureRank(left) - architectureRank(right)); +} + +function collectAssetArchitectures( + platformId: PlatformDownload["id"], + asset: PlatformDownloadAsset, +): ReleaseArchitecture[] { + if (asset.architectures.length === 0) { + throw new Error(`Release asset ${platformId}/${asset.id} must declare at least one architecture.`); + } + + for (const architecture of asset.architectures) { + if (!isReleaseArchitecture(architecture)) { + throw new Error(`Release asset ${platformId}/${asset.id} declares unsupported architecture: ${architecture}`); + } + } + + return asset.architectures; +} + +function isReleaseArchitecture(value: string): value is ReleaseArchitecture { + return ARCHITECTURE_ORDER.includes(value as ReleaseArchitecture); +} + +function architectureRank(architecture: ReleaseArchitecture): number { + return ARCHITECTURE_ORDER.indexOf(architecture); +} + +function formatArchitectureList( + architectures: ReleaseArchitecture[], + style: "and" | "comma" = "and", +): string { + if (architectures.length === 0) { + throw new Error("Release download metadata must include at least one architecture."); + } + if (style === "comma") { + return architectures.join(", "); + } + if (architectures.length === 1) { + return architectures[0]; + } + if (architectures.length === 2) { + return `${architectures[0]} and ${architectures[1]}`; + } + return `${architectures.slice(0, -1).join(", ")}, and ${architectures.at(-1)}`; +} diff --git a/scripts/generate-landing-release-links.mjs b/scripts/generate-landing-release-links.mjs index 9c9d6d28..face9b1e 100644 --- a/scripts/generate-landing-release-links.mjs +++ b/scripts/generate-landing-release-links.mjs @@ -123,14 +123,18 @@ async function main() { ]); const content = `export type PlatformDownloadAsset = { + architectures: ReleaseArchitecture[]; id: string; label: string; href: string; note: string; }; +export type ReleaseArchitecture = "arm64" | "x64" | "x86"; + export type PlatformDownload = { id: "macos" | "windows" | "linux"; + packageDescription: string; title: string; note: string; assets: PlatformDownloadAsset[]; @@ -141,16 +145,19 @@ export const releaseVersion = "${version}"; export const platformDownloads: PlatformDownload[] = [ { id: "macos", + packageDescription: "signed and notarized DMG builds", title: "macOS", note: "Signed + notarized DMG builds", assets: [ { + architectures: ["arm64"], id: "macos-arm64-dmg", label: "Apple Silicon DMG", href: "${urlFor(baseUrl, macArm64Dmg)}", note: "arm64", }, { + architectures: ["x64"], id: "macos-x64-dmg", label: "Intel Mac DMG", href: "${urlFor(baseUrl, macIntelDmg)}", @@ -160,10 +167,12 @@ export const platformDownloads: PlatformDownload[] = [ }, { id: "windows", + packageDescription: "an NSIS installer build", title: "Windows", note: "NSIS installer builds", assets: [ { + architectures: ["arm64", "x64"], id: "windows-universal-installer", label: "Windows Universal Installer", href: "${urlFor(baseUrl, windowsUniversalInstaller)}", @@ -173,40 +182,47 @@ export const platformDownloads: PlatformDownload[] = [ }, { id: "linux", + packageDescription: "AppImage, DEB, and RPM builds", title: "Linux", note: "AppImage, DEB, and RPM builds", assets: [ { + architectures: ["x64"], id: "linux-x64-appimage", label: "Linux x64 AppImage", href: "${urlFor(baseUrl, linuxX64AppImage)}", note: "x64", }, { + architectures: ["x64"], id: "linux-x64-deb", label: "Linux x64 DEB", href: "${urlFor(baseUrl, linuxX64Deb)}", note: "x64", }, { + architectures: ["x64"], id: "linux-x64-rpm", label: "Linux x64 RPM", href: "${urlFor(baseUrl, linuxX64Rpm)}", note: "x64", }, { + architectures: ["arm64"], id: "linux-arm64-appimage", label: "Linux ARM64 AppImage", href: "${urlFor(baseUrl, linuxArm64AppImage)}", note: "arm64", }, { + architectures: ["arm64"], id: "linux-arm64-deb", label: "Linux ARM64 DEB", href: "${urlFor(baseUrl, linuxArm64Deb)}", note: "arm64", }, { + architectures: ["arm64"], id: "linux-arm64-rpm", label: "Linux ARM64 RPM", href: "${urlFor(baseUrl, linuxArm64Rpm)}", diff --git a/tests/unit/landing-platform-support.test.ts b/tests/unit/landing-platform-support.test.ts new file mode 100644 index 00000000..b398f7b1 --- /dev/null +++ b/tests/unit/landing-platform-support.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { platformDownloads, type PlatformDownload } from "../../landing/components/release-links.generated"; +import { buildPlatformSupportSummary } from "../../landing/lib/platform-support"; + +describe("landing platform support copy", () => { + it("derives FAQ support copy from current generated release assets", () => { + const summary = buildPlatformSupportSummary(platformDownloads); + + expect(summary.downloadAnswer).toBe( + "Use the download section on this page to choose the build that matches your architecture and package format. " + + "macOS ships as signed and notarized DMG builds for arm64 and x64. " + + "Windows ships as an NSIS installer build for arm64 and x64. " + + "Linux ships as AppImage, DEB, and RPM builds for arm64 and x64.", + ); + expect(summary.platformAnswer).toBe( + "AgentCompany is available on macOS (arm64, x64), Windows (arm64, x64), " + + "and Linux (arm64, x64). It is built with Electron and the desktop feature set stays aligned across all supported targets.", + ); + }); + + it("uses structured architectures and platform package notes", () => { + const downloads = [ + platform("macos", "macOS", "PKG builds", "PKG Builds", [["Apple Silicon", ["arm64"], "display-only note"]]), + platform("windows", "Windows", "an MSIX build", "MSIX Builds", [["Universal", ["arm64", "x64"], "ARM64 + x64"]]), + platform("linux", "Linux", "tarball builds", "Tarball Builds", [["Linux bundle", ["x64"], "x64 (glibc)"]]), + ]; + + const summary = buildPlatformSupportSummary(downloads); + + expect(summary.downloadAnswer).toBe( + "Use the download section on this page to choose the build that matches your architecture and package format. " + + "macOS ships as PKG builds for arm64. " + + "Windows ships as an MSIX build for arm64 and x64. " + + "Linux ships as tarball builds for x64.", + ); + expect(summary.platformAnswer).toBe( + "AgentCompany is available on macOS (arm64), Windows (arm64, x64), and Linux (x64). " + + "It is built with Electron and the desktop feature set stays aligned across all supported targets.", + ); + }); + + it("keeps generated asset notes synchronized with structured architectures", () => { + for (const platformDownload of platformDownloads) { + for (const asset of platformDownload.assets) { + expect(asset.note).toBe(asset.architectures.join(" + ")); + } + } + }); + + it("fails when generated release assets omit architecture metadata", () => { + const downloads = [ + platform("macos", "macOS", "DMG builds", "DMG Builds", [["Apple Silicon", [], "arm64"]]), + platform("windows", "Windows", "an NSIS installer build", "NSIS Builds", [["Universal", ["arm64", "x64"], "arm64 + x64"]]), + platform("linux", "Linux", "AppImage builds", "AppImage Builds", [["Linux", ["x64"], "x64"]]), + ]; + + expect(() => buildPlatformSupportSummary(downloads)).toThrow( + "Release asset macos/macos-apple-silicon must declare at least one architecture.", + ); + }); +}); + +function platform( + id: PlatformDownload["id"], + title: string, + packageDescription: string, + note: string, + assets: Array<[label: string, architectures: PlatformDownload["assets"][number]["architectures"], note: string]>, +): PlatformDownload { + return { + id, + packageDescription, + title, + note, + assets: assets.map(([label, architectures, assetNote]) => ({ + architectures, + id: `${id}-${label.toLowerCase().replaceAll(/\s+/g, "-")}`, + label, + href: `https://download.example/${id}`, + note: assetNote, + })), + }; +}