Files

198 lines
8.0 KiB
TypeScript

/**
* Every way to get Hanzo, in one list — the model behind the front page.
*
* A surface appears here only if something real is behind it: a resolved
* release asset, or a host that answers. The URLs are NOT written here — they
* come from `data/releases.json`, which `scripts/sync-releases.mjs` resolves
* against the actual releases on every build. So a platform whose build stopped
* publishing stops rendering, rather than becoming a button that 404s.
*
* "Something real" means something a READER can take, which is stricter than it
* sounds and is where this list was wrong until 2026-08-27. The sync used to
* resolve with CI's `GITHUB_TOKEN`, so hanzoai/extension — a PRIVATE repo —
* resolved all 23 of its assets at build time and answered 404 for every
* visitor who clicked one. Six dead links were live on the page. The sync now
* asks anonymously, as a reader does, and `offered` below drops a surface that
* resolved nothing, so neither half can publish a download nobody can take.
*
* There is still no mobile card. hanzoai/ios and hanzoai/android hold real
* Expo/RN apps and are actively worked on, but they have published no release,
* and nothing is on the App Store or Google Play (checked 2026-08-27: zero
* store listings, and ai.hanzo.app / ai.hanzo / com.hanzo.app all 404 on Play).
* A tile that says "coming soon" is an apology occupying the place of a
* product. When a build ships, add the pattern to the sync and a card here.
*/
import { AppWindow, Code, Command, Compass, Flame, MousePointer2, Braces, Terminal, Wind, type LucideIcon } from 'lucide-react'
import { Chrome } from '@/components/brand'
import releases from './releases.json'
/** One row: a platform, and the artifact it downloads. */
export interface Build {
name: string
url: string
/** Bytes, from the release payload — shown so a reader knows what they are taking. */
size: number
icon?: LucideIcon
}
export interface Surface {
id: string
name: string
/** What it is, in the reader's words. */
blurb: string
/** A real capture of this surface doing its job. Omitted when we have none. */
visual?: string
/** The version behind the builds, when they come from a release. */
version?: string
/** One command that installs it, for the surfaces that have one. */
command?: string
/** Per-platform downloads, as rows. */
builds?: Build[]
/** A hosted surface opens rather than downloads. */
open?: { href: string; label: string }
/** Where the full list of builds lives. */
all?: string
}
// `version` is optional because a surface whose repo serves the public nothing
// has no public version to name — the sync writes the repo and an empty
// platform set, and the card never renders.
type Resolved = Record<string, { repo: string; version?: string; platforms: Record<string, { url: string; size: number }> }>
const R = releases as Resolved
/** Rows for a resolved surface, in the declared order, skipping what did not publish. */
const builds = (id: string, order: [string, LucideIcon?][]): Build[] =>
order
.filter(([name]) => R[id]?.platforms[name])
.map(([name, icon]) => ({ name, icon, ...R[id].platforms[name] }))
/**
* The order is the order a reader wants them: the one binary that does the most
* first, then the browser that needs no binary at all, then the hosted rooms,
* then the app on their machine, then the two that install into something they
* already have open.
*
* This is the DECLARED list. What renders is `SURFACES` below, which is this
* filtered to what resolved — so a surface stays written down here, with its
* copy intact, through a spell of publishing nothing.
*/
const ALL: Surface[] = [
{
id: 'cli',
name: 'Hanzo CLI',
blurb:
'The coding agent, in your terminal. It reads the repo you are in, writes and runs code, and drives every product of the Hanzo cloud from one binary.',
command: 'curl -fsSL hanzo.sh | sh',
version: R.cli?.version,
visual: '/download/cli.png',
builds: builds('cli', [
['macOS (Apple Silicon)', Command],
['macOS (Intel)', Command],
['Windows', AppWindow],
['Linux (x86_64)', Terminal],
['Linux (arm64)', Terminal],
]),
all: 'https://github.com/hanzoai/cli/releases/latest',
},
{
// The way in that installs nothing. hanzo.app hands out the builds; the
// browser needs none, so this card opens rather than downloads — and it is
// second because "you already have what you need" is the shortest answer on
// the page for a reader who does not want a binary at all.
id: 'web',
name: 'Web',
blurb:
'Nothing to install. Sign in at hanzo.ai and everything on this page is already open in the browser you have — same account, same key as every build here.',
open: { href: 'https://hanzo.ai', label: 'Open hanzo.ai' },
},
{
id: 'team',
name: 'Hanzo Team',
blurb:
'Chat, channels and threads for your whole company, with Hanzo in the room. Ask it to summarize a channel or answer in-thread, and it already has your org context.',
open: { href: 'https://hanzo.team', label: 'Open Hanzo Team' },
},
{
id: 'desktop',
name: 'Desktop',
blurb:
'A native app with a global hotkey. Talk to Hanzo from any window, capture a region of the screen, and put the answer back where you were typing.',
version: R.desktop?.version,
builds: builds('desktop', [
['macOS', Command],
['Windows', AppWindow],
['Linux (AppImage)', Terminal],
['Linux (deb)', Terminal],
['Linux (arm64)', Terminal],
]),
},
{
id: 'studio',
name: 'Hanzo Studio',
blurb:
'Build, run and ship AI apps in the browser. Compose agents, wire your models and data, and deploy without leaving the page.',
open: { href: 'https://studio.hanzo.ai', label: 'Open Studio' },
},
{
id: 'browser',
name: 'Browser extension',
blurb:
'Ask Hanzo about the page you are on. It reads the tab for context, captures what you select, and runs agents from the toolbar.',
version: R.browser?.version,
builds: builds('browser', [
['Chrome', Chrome],
['Firefox', Flame],
['Safari', Compass],
['Edge', AppWindow],
]),
},
{
id: 'editor',
name: 'Editors',
blurb:
'Completions, chat and agentic edits where you already write code. One extension, every editor that takes one.',
version: R.editor?.version,
builds: builds('editor', [
['VS Code', Code],
['Cursor', MousePointer2],
['Windsurf', Wind],
['JetBrains', Braces],
]),
},
]
/**
* A surface a reader can act on: it downloads something, or it opens somewhere.
*
* The doctrine at the top of this file has always said a surface renders only
* if something real is behind it, but until now only the BUILD ROWS were
* filtered — `builds()` returned an empty list and the card still drew, so a
* surface whose repo published nothing rendered as a name, a blurb and no way
* to get it. That is the "coming soon" tile this file refuses, arrived at by
* accident instead of on purpose.
*/
const offered = (s: Surface): boolean => Boolean(s.builds?.length || s.open)
/** What a reader can actually get today. */
export const SURFACES: Surface[] = ALL.filter(offered)
/** The one surface the page leads with, and the rest as a grid. */
export const [LEAD, ...REST] = SURFACES
/**
* The native build hanzo.app offers as Download: the desktop app once its
* builds are public, and until then the one binary that is — the CLI, which is
* the same agent on the reader's own machine. Chosen from what resolved, so it
* is always a file a reader can take.
*/
export const NATIVE: Surface | undefined = SURFACES.find((s) => s.id === 'desktop') ?? SURFACES.find((s) => s.id === 'cli')
/** The CLI's install line, the one the download page prints. */
export const INSTALL: string | undefined = SURFACES.find((s) => s.id === 'cli')?.command
/** Bytes as a reader reads them. */
export const weigh = (bytes: number): string =>
bytes >= 1 << 20 ? `${Math.round(bytes / (1 << 20))} MB` : `${Math.round(bytes / 1024)} KB`