mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
198 lines
8.0 KiB
TypeScript
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`
|