- @hanzo/gui + whole @hanzogui/* graph 7.3.0 -> 8.0.0, @hanzo/ui -> 8.0.38, @hanzo/design -> 0.3.3; [email protected].5 declared directly ([email protected] imports it but no longer declares the peer). - Icons from the package root, not /icons/* subpaths: 8.0.0's per-icon exports entries carry no types condition, so subpaths typecheck as any. Root export is typed and sideEffects:false tree-shakes — JS bundle 647 kB -> 531 kB. - gui base sheet as a REAL cacheable stylesheet: scripts/gen-gui-css.mjs (first step of pnpm build) writes config.getCSS() to src/gui.css, main.tsx imports it, GuiProvider renders disableInjectCSS. ~43 kB gzip, fingerprinted. - TypeScript 7.0.2 for tsc; .pnpmfile.cjs gives @typescript-eslint/* their own [email protected] (side-by-side per typescript-eslint#10940) so lint runs. - next-theme shim STAYS: 8.0.38 lazily code-splits ThemeToggleNext, but Rollup still follows the dynamic import and the optional peer breaks the build (measured). SiteNav still authors $sm as phone, so the sm override stays too. - Measured: typecheck 0, lint 0, build 0, polyglot bash -n OK + --help prints usage, page verified at 1280px and 390px. Co-authored-by: Hanzo Dev <[email protected]>
8.6 KiB
hanzo.sh
The install surface: one page that is also one installer. curl hanzo.sh | bash
installs the Hanzo toolkit; opening hanzo.sh in a browser renders the landing
page. Both are the same file.
The polyglot
pnpm build is vite build && node scripts/build-polyglot.js. The second step
rewrites dist/index.html into a file that is simultaneously:
- a shell script — line 1 is
#!/bin/sh, line 2 opens a<<\EOFheredoc that swallows the whole HTML document, and the installer (public/install.sh, minus its own shebang) follows the closingEOF; - an HTML document — the heredoc content is the real page, and
</html>is rewritten to</html><!--so the shell half is an HTML comment.
The installer body uses bash arrays, so the contract is curl hanzo.sh | bash,
not | sh. The Dockerfile asserts all three properties (shebang, <!DOCTYPE html>, bash -n) and fails the build if any is lost — nothing downstream can
catch it, because everything downstream is bytes.
public/install.sh is also served at /install.sh for anyone who wants the
installer alone; public/{bot,cli,agents,dev,full,mcp,node,python,rust} are
per-bundle shims.
Serving chain
push to main
-> .github/workflows/sync.yml carries refs to git.hanzo.ai (nothing else)
-> .hanzo/workflows/deploy.yml runs natively on the forge
docker build . Dockerfile: pnpm build -> polyglot assert
-> FROM ghcr.io/hanzoai/static:v0.5.1
docker push ghcr.io/hanzoai/hanzo-sh:<short-sha>
-> hanzoai/universe infra/k8s/operator/crs/hanzo-sh.yaml
spec.image.tag: <short-sha> set by a human, never by the build
-> hanzoai/ingress hanzo.sh -> Service hanzo-sh:80 -> :3000
hanzoai/static is a Go binary on scratch. It defaults to -port 3000 -root /public and answers with http.ServeContent, so a file goes out byte-for-byte
with no rewrite, minify or injection — which is the only reason a polyglot can be
served from an ordinary static server at all. It runs WITHOUT -spa: this app has
exactly one route, so a mistyped path must 404 rather than return the installer.
HANZO_STATIC_CSP in the CR is a response header only; it cannot touch the body.
It sets script-src 'self', which the page now satisfies outright: the
third-party cdn.gpteng.co/gptengineer.js tag is gone from index.html (it was
a build-tool artifact the CSP was already blocking and nothing depended on), and
so is the CDN font @import.
Not live from here yet
hanzo.sh is served today by a Cloudflare assets Worker named hanzo-sh
(wrangler.toml, custom_domain = true), published by hand with
pnpm build && npx wrangler@3 deploy. wrangler.toml stays until the image is
promoted; deleting it first would strand the live host.
Two things must be reconciled BEFORE anyone pins a tag in the CR:
- The CSP can come down to same-origin.
mainused to@importGeist fromcdn.jsdelivr.net, which is why the CR's CSP allows that host for styles and fonts. It does not any more:@hanzo/designships both Geist faces aswoff2inside the package and the build emits them intodist/assets/, so the page makes no third-party request at all. Dropcdn.jsdelivr.netfromHANZO_STATIC_CSPwhen the image is promoted. - Promotion order. publish an image -> set
spec.image.tagincrs/hanzo-sh.yaml-> add- hanzo-sh.yamltocrs/kustomization.yaml-> confirm the pod is Running and thatcurl <pod>/ | bash -s -- --helpprints the installer usage -> only then repoint hanzo.sh DNS off the Worker -> then deletewrangler.toml.
The CR is committed INERT (empty tag, absent from kustomization.yaml).
Promoting an App with no image tag takes the host down instead of leaving it
alone.
Stack
Vite 5 + React 19 on @hanzo/ui 8.0.38 over the @hanzo/gui 8.0.0 backend
(whole @hanzogui/* graph at 8.0.0). One route (src/pages/Index.tsx), no
router — hanzoai/static runs without -spa, so the routing decision is
already made and a client-side router would only pretend to make it again.
pnpm 9; pnpm-lock.yaml is what the build resolves (--frozen-lockfile).
TypeScript is 7.0.2 (the native compiler) and pnpm typecheck runs it
directly. typescript-eslint supports only the TS 6 API, so .pnpmfile.cjs
gives the @typescript-eslint/* packages [email protected] as their OWN
dependency — the documented side-by-side arrangement — while everything else
sees 7.0.2. Delete that file when typescript-eslint supports TS >= 7.1
(typescript-eslint#10940).
The gui base sheet is a REAL stylesheet, not runtime injection:
scripts/gen-gui-css.mjs (first step of pnpm build) bundles
src/gui.config.ts with esbuild and writes config.getCSS() to src/gui.css,
main.tsx imports it, and GuiProvider renders with disableInjectCSS. Vite
fingerprints the sheet (~1.37 MB raw, ~43 kB gzip) so the browser caches it
once. The generated file is committed so dev/typecheck work without a build.
Icons import from the @hanzogui/lucide-icons-2 package ROOT, not
/icons/<Name> subpaths: 8.0.0's per-icon exports entries carry no types
condition (the .d.ts files exist under types/icons/ but are unreachable),
so subpath imports typecheck as any. The root export is typed, and
sideEffects: false means Vite tree-shakes it to the same bytes — measured,
the JS bundle SHRANK 647 kB -> 531 kB moving to 8.0.0 + root imports.
react-native-svg is declared directly because @hanzogui/[email protected]
imports it without declaring the peer.
pnpm install
pnpm dev # vite, :8080
pnpm build # -> dist/, including the polyglot rewrite
pnpm typecheck
pnpm lint
scripts/polyglot.sh is an unwired duplicate of scripts/build-polyglot.js;
only the .js is in pnpm build.
Where the page comes from
There is no Tailwind, no shadcn and no Radix; the 48 generated components/ui/*
files and the 27 Radix packages behind them are gone.
- @hanzo/products — the header's launcher and the whole footer index. The
content of both is settled once for the estate; hanzo.sh only declares its own
four nav links and its one call-to-action (
src/site.ts), because@hanzo/[email protected]has nohanzo.shSiteIdyet. When it does,HEADERcollapses toHEADERS['hanzo.sh']. - @hanzo/ui/product —
SiteNav,SiteFooter,BrandMark,Panel,StatusTag,ToastProvider. The page used to draw two headers and two footers (aNavbarabove aHerothat carried its own header, and a hand-written footer ofhref="#"links below the real one); these are the components that end that. - @hanzo/gui — every primitive under it (
YStack/XStack/Text/Anchor/Separator), on the shared scale from@hanzo/ui/gui-config. - @hanzo/design — the token layer: monochrome, dark by default, Geist Sans
and Geist Mono self-hosted in the package.
src/index.cssimports it and does nothing else. - @hanzo/logo — the mark, through
BrandMark. Not a direct dependency:@hanzo/uialready depends on it, and declaring a package nothing here imports is how a dependency list starts lying.@hanzo/brandis deliberately absent too — it is the MULTI-brand registry, and hanzo.sh is one brand. Reaching for it here is how a Lux or Zoo mark ends up on a Hanzo surface.
src/site.ts is the page as data; src/components/ renders it. Responsiveness
is flexWrap + minW, not breakpoints: the same tree reflows from 390px to
1280px with one media prop in the whole app.
Two workarounds, both with a delete condition
@hanzo/[email protected] still has two defects this app has to route around. Both are
marked in place; remove them when the package is fixed.
src/shims/hanzogui-next-theme.ts— 8.0.38 improved on 8.0.26:ThemeTogglenow lazily code-splitsThemeToggleNextbehind an error boundary instead of importing it statically. But Rollup still follows the dynamic import at build time, andThemeToggleNextstatically imports@hanzogui/next-theme(an optional peer) — without the alias the build fails on"useThemeSetting" is not exported by "__vite-optional-peer-dep:..."(measured). Sovite.config.tskeeps aliasing the specifier to a stub; nothing here renders a theme toggle. Delete when a Vite build passes without it.src/gui.config.ts—SiteNavstill writes$smmeaning "phone" in 8.0.38 (verified indist/product/SiteNav.js), but v5 definessmasminWidth: 640, so on the stock scale a phone gets the desktop link row and a desktop gets a hamburger. This app redefines that ONE key asmaxWidth: 639.98; nothing else it renders readssm.