one URL, two answers: the page and the installer stop being the same file

dist/index.html was a polyglot — `#!/bin/sh`, a `<<\EOF` heredoc swallowing the
whole document, the installer after the closing EOF. Both halves ran. The
browser half was never honest, and could not be made honest.

Anything before <!DOCTYPE html> that is not whitespace or a comment puts the
document in quirks mode. Chromium, at 390 and 1280: compatMode BackCompat;
document.head.children.length === 1, with all fifteen head elements — title,
icons, viewport, description, every og: and twitter: tag — parsed into <body>
where they do nothing; and "#!/bin/sh <<\EOF" as the first text node of the
page, first string in body.innerText, pushing <main> down 24px. Proven by
re-serving the identical live bytes with the 17-byte prefix stripped: BackCompat
-> CSS1Compat, head.children 1 -> 15, at both widths.

No arrangement of those bytes avoids it. Only whitespace and comments may
precede a DOCTYPE; an HTML comment opens with `<!`, which a shell reads as a
redirect from a file named `!--`, so a POSIX script cannot start there at all.
The trick was unfixable, not unlucky.

So the two resources become two files and `/` chooses between them by Accept,
which is what HTTP has for this. worker.js is the whole rule: text/html gets
dist/page.html, anything else — curl, wget, the /cli /mcp /dev shims that
re-exec `curl -fsSL https://hanzo.sh` — gets dist/install.sh. `/` carries
vary: accept and cache-control: no-store, because Cloudflare honours Vary only
on Accept-Encoding and a shared cache holding one representation for the other
kind of client breaks either the page or the install command.

scripts/postbuild.js replaces the polyglot rewrite: it moves the document off
index.html (Static Assets serve an exact match BEFORE the Worker runs, so a file
at that name would hand curl the HTML) and writes /install from install.sh, so
the alias is one file in git instead of 12,746 duplicated bytes.

wrangler.toml: routes moves ABOVE [assets]. TOML puts every key after a table
header inside that table, so it was parsing as assets.routes and wrangler
dropped it with a warning — the custom domain survived only because it was
attached out of band. html_handling and not_found_handling are "none": exact
match or the Worker, nothing silently rewritten, a mistyped path still 404s.

The deploy gate now checks both representations against the live URL, plus a
200 for every path the head declares — the check that would have caught
og-image.svg. Verified against the real runtime before pushing: wrangler dev
--local, curl gets install.sh byte-for-byte, a browser gets page.html with
<!DOCTYPE html> at byte 0, `sh -n -c "$(curl -fsSL /)"` parses under dash and
bash, and /index.html /nonexistent-xyz /some/deep/path still 404.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-05 16:16:34 -07:00
co-authored by Hanzo Dev
parent 09b1fada81
commit efd925d977
8 changed files with 148 additions and 411 deletions
+52 -34
View File
@@ -1,20 +1,18 @@
name: deploy
# hanzo.sh is served by the Cloudflare assets Worker named `hanzo-sh`, and until
# this file existed it was published BY HAND (`pnpm build && npx wrangler@3
# deploy`). That is why a correct fix could sit merged on main for weeks while
# `curl hanzo.sh | sh` kept handing out the old installer: the repo was right and
# the bytes users got were not. A host that only updates when someone remembers
# is a host that is stale by default.
# hanzo.sh is served by the Cloudflare Worker named `hanzo-sh` over its own
# static assets, and until this file existed it was published BY HAND (`pnpm
# build && npx wrangler@3 deploy`). That is why a correct fix could sit merged on
# main for weeks while `curl hanzo.sh | sh` kept handing out the old installer:
# the repo was right and the bytes users got were not. A host that only updates
# when someone remembers is a host that is stale by default.
#
# So: every push to main republishes, and the job REFUSES to pass unless the live
# URL actually serves the new bytes. Deploying without checking is how you get a
# green build over a stale site — the exact failure this replaces.
# URL actually serves the new bytes — in BOTH representations, because `/` is two
# resources: the installer to curl, the document to a browser.
#
# Runs on GitHub's runner: this org registers no self-hosted runner, and the only
# copy of the Cloudflare credential lives in GitHub org secrets. It builds no
# container image — the image path (Dockerfile -> hanzoai/static -> universe CR)
# is a separate, unfinished migration documented in LLM.md.
# copy of the Cloudflare credential lives in GitHub org secrets.
on:
push:
@@ -47,19 +45,23 @@ jobs:
- run: pnpm build
# The same four asserts the Dockerfile makes. dist/index.html is a polyglot
# — an HTML document AND the installer — and both halves have to survive.
# dash as well as bash: the installer is POSIX and the page publishes
# `| sh`, and a bash-only check is what let `set -o pipefail` ship in a file
# whose first line says #!/bin/sh.
- name: the polyglot is still both things
# Two files, each whole. The document must open with <!DOCTYPE html> at
# byte 0 or the browser parses it in quirks mode and drops every <head>
# element into <body>; the installer must parse under dash AND bash,
# because the page publishes `| sh` and readers type `| bash` anyway — a
# bash-only check is what let `set -o pipefail` ship in a file whose first
# line says #!/bin/sh. dist/index.html must NOT exist: an asset at that
# name is served before the Worker runs, and curl would get the document.
- name: the page is a document and the installer is a script
run: |
set -euo pipefail
head -1 dist/index.html | grep -qx '#!/bin/sh'
grep -qx '<!DOCTYPE html>' dist/index.html
dash -n dist/index.html
bash -n dist/index.html
! grep -qiE 'astral\.sh|uv tool install' dist/index.html
[ "$(head -c 15 dist/page.html)" = '<!DOCTYPE html>' ]
[ ! -e dist/index.html ]
head -1 dist/install.sh | grep -qx '#!/bin/sh'
dash -n dist/install.sh
bash -n dist/install.sh
cmp dist/install dist/install.sh
! grep -qiE 'astral\.sh|uv tool install' dist/install.sh
- name: publish
env:
@@ -67,9 +69,9 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: npx wrangler@3 deploy
# Cloudflare answers this host with `cf-cache-status: HIT`, so a fresh
# deploy is not the same thing as a fresh response. Best-effort: the token
# may not carry Cache Purge, and the check below is what actually decides.
# `/` is now no-store, but the assets behind it are cached at the edge.
# Best-effort: the token may not carry Cache Purge, and the check below is
# what actually decides.
- name: purge the edge
continue-on-error: true
env:
@@ -86,24 +88,40 @@ jobs:
"https://api.cloudflare.com/client/v4/zones/$zone/purge_cache" | head -c 300
# The only statement that matters: what does the live URL hand a stranger?
# Twice — curl asks for anything, a browser asks for text/html.
- name: the live bytes are the new bytes
run: |
set -euo pipefail
want=$(md5sum dist/index.html | cut -d' ' -f1)
want_sh=$(md5sum dist/install.sh | cut -d' ' -f1)
want_html=$(md5sum dist/page.html | cut -d' ' -f1)
for i in $(seq 1 30); do
got=$(curl -sS https://hanzo.sh | md5sum | cut -d' ' -f1)
[ "$got" = "$want" ] && break
got_sh=$(curl -sS https://hanzo.sh | md5sum | cut -d' ' -f1)
got_html=$(curl -sS -H 'Accept: text/html' https://hanzo.sh | md5sum | cut -d' ' -f1)
[ "$got_sh" = "$want_sh" ] && [ "$got_html" = "$want_html" ] && break
sleep 10
done
echo "before : ${{ steps.before.outputs.md5 }}"
echo "built : $want"
echo "live : $got"
[ "$got" = "$want" ] || {
echo "::error::hanzo.sh is still serving different bytes than were just built"
exit 1; }
echo "before : ${{ steps.before.outputs.md5 }}"
echo "installer : built $want_sh live $got_sh"
echo "document : built $want_html live $got_html"
[ "$got_sh" = "$want_sh" ] || {
echo "::error::curl hanzo.sh is not the installer that was just built"; exit 1; }
[ "$got_html" = "$want_html" ] || {
echo "::error::a browser at hanzo.sh is not getting the page that was just built"; exit 1; }
# And it is still an installer, not just matching bytes.
curl -sS https://hanzo.sh -o /tmp/live.sh
dash -n /tmp/live.sh
! grep -qiE 'astral\.sh|uv tool install' /tmp/live.sh
# Every path the page or the HTML head names. og:image used to point at
# /og-image.svg, which was never in the repo — every social card was a
# 404 and nothing said so.
for p in /favicon.ico /favicon.svg /favicon-32x32.png /favicon-16x16.png \
/apple-touch-icon.png /og-image.png /fonts/Geist-Variable.woff2 \
/fonts/GeistMono-Variable.woff2 /install /install.sh /llms.txt \
/cli /mcp /dev /full /bot /desktop /node; do
code=$(curl -sS -o /dev/null -w '%{http_code}' "https://hanzo.sh$p")
[ "$code" = 200 ] || { echo "::error::hanzo.sh$p is $code"; exit 1; }
done
echo "hanzo.sh serves the build from ${GITHUB_SHA:0:8}"
+3
View File
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# wrangler dev/deploy scratch
.wrangler/
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node scripts/build-polyglot.js",
"build": "vite build && node scripts/postbuild.js",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
-328
View File
@@ -1,328 +0,0 @@
#!/bin/sh
# hanzo installer
#
# curl -fsSL hanzo.sh | sh
# curl -fsSL hanzo.sh | bash
#
# copyright (c) 2024-2026 hanzo ai inc.
# https://hanzo.ai
#
# Downloads one prebuilt native binary per tool, verifies its sha256, and puts it
# on PATH. That is the whole design. Nothing is built here, no package manager is
# involved, and there is no runtime to install first.
#
# POSIX sh on purpose. The site published `| bash` and llms.txt published `| sh`,
# and the script was bash-only — so every reader who copied the `sh` form (every
# agent reading llms.txt) got `set: Illegal option -o pipefail` from dash and
# nothing installed. Rather than pick a winner and leave the other broken, this
# runs under both: no arrays, no [[ ]], no <<<, no echo -e, no pipefail. The
# polyglot the site serves already declares #!/bin/sh, so this also makes that
# shebang honest.
#
# It does NOT re-implement downloading. hanzoai/cli/install.sh is the one
# implementation of "fetch a Hanzo binary" — platform detection, asset naming,
# checksum verification and the second-name symlink all live there, once. This
# fetches it and drives it once per tool. Two copies of platform detection would
# be the same class of bug as two copies of a route table.
#
# Every tool below is a published, public, checksummed native binary. A tool that
# is not one is NOT installed by some other mechanism to pad the list — it is
# named, with the reason. An installer that quietly reaches for a package manager
# to look complete is the defect this file exists to remove.
set -eu
HANZO_DIR="${HANZO_INSTALL_DIR:-$HOME/.local/bin}"
HANZO_QUIET="${HANZO_QUIET:-0}"
CLI_REPO="${HANZO_CLI_REPO:-hanzoai/cli}"
# The complete set of Hanzo tools that ship as a public, checksummed, native
# binary today, as <name>:<repo>:<binary>:<second-name>.
#
# The binary is also the asset prefix and the name inside the tarball — the
# convention hanzoai/cli/install.sh relies on. Adding a tool is one line, the day
# its repo is public and publishes <binary>-<os>-<arch>.tar.gz.sha256.
#
# `dev` takes no second name on purpose. cli/install.sh defaults the alias to
# `hanzo-node`, which the CLI already owns — giving it to `dev` too would point
# the delegate name at a different program.
TOOLS='hanzo:hanzoai/cli:hanzo:hanzo-node
mcp:hanzoai/mcp:hanzo-mcp:mcp
dev:hanzoai/dev:dev:'
# The installable names, read off TOOLS so no message can name a different set
# than the loop installs.
tool_names() {
printf '%s\n' "$TOOLS" | while IFS= read -r n_row; do
printf ' %s' "${n_row%%:*}"
done
}
# Named, not silently skipped, and never substituted with something else.
unavailable_rows() {
cat <<'ROWS'
node|source is not public yet
desktop|`hanzo desktop` is in the CLI; the standalone app is not public yet
bot|`hanzo bot` is in the CLI; the standalone node is not native yet
ROWS
}
if [ -t 1 ] && [ "${TERM:-}" != dumb ]; then
R=$(printf '\033[0;31m'); G=$(printf '\033[0;32m'); Y=$(printf '\033[0;33m')
C=$(printf '\033[0;36m'); N=$(printf '\033[0m'); BD=$(printf '\033[1m')
DM=$(printf '\033[2m')
else
R=''; G=''; Y=''; C=''; N=''; BD=''; DM=''
fi
INSTALLED_N=0
FAILED_N=0
FAILED_LIST=''
log() { [ "$HANZO_QUIET" = 1 ] || printf ' %s\n' "$1"; }
ok() { printf ' %s✓%s %s\n' "$G" "$N" "$1"; }
warn() { printf ' %s!%s %s\n' "$Y" "$N" "$1"; }
fail() { printf ' %s✗%s %s\n' "$R" "$N" "$1"; }
die() { fail "$1"; exit 1; }
banner() {
[ "$HANZO_QUIET" = 1 ] && return 0
printf '%s' "$DM"
cat <<'ART'
__
/ /_ ____ _____ ____ ____
/ __ \/ __ `/ __ \/_ / / __ \
/ / / / /_/ / / / / / /_/ /_/ /
/_/ /_/\__,_/_/ /_/ /___/\____/
ART
printf '%s\n' "$N"
}
usage() {
cat <<'EOF'
hanzo installer
usage: curl -fsSL hanzo.sh | sh
curl -fsSL hanzo.sh | sh -s -- [options] [tool...]
Downloads one prebuilt native binary per tool and verifies its checksum.
With no tool named, installs every tool that ships as a public native binary.
tools:
hanzo the Hanzo CLI (also installed as hanzo-node, the same build)
mcp the MCP server (also installed as hanzo-mcp, the same build)
dev Hanzo Dev, the coding agent `hanzo code` runs by default
options:
-d, --dir PATH install directory (default: ~/.local/bin)
--version TAG pin the CLI release (default: latest)
-q, --quiet less output
-h, --help show this help
shortcuts:
curl -fsSL hanzo.sh | sh # every installable tool
curl -fsSL hanzo.sh/cli | sh # the CLI only
curl -fsSL hanzo.sh/mcp | sh # the MCP server only
Re-run any time to upgrade; installing is just downloading the current build.
EOF
exit 0
}
WANT=''
parse_args() {
while [ $# -gt 0 ]; do
case $1 in
--dir|-d) HANZO_DIR="$2"; shift 2 ;;
--version) HANZO_VERSION="$2"; export HANZO_VERSION; shift 2 ;;
--quiet|-q) HANZO_QUIET=1; shift ;;
--help|-h) usage ;;
# Retired flags. Installing IS upgrading — there is one path and it
# always fetches the current build — so these mean nothing now.
# Accept and ignore rather than failing an install someone scripted.
--upgrade|-u|--force|-f) shift ;;
--bundle|-b) WANT="$WANT $2"; shift 2 ;;
-*) warn "ignoring unknown option: $1"; shift ;;
*) WANT="$WANT $1"; shift ;;
esac
done
}
# wanted <name> <binary> — did the caller ask for this tool?
# `default`, `full` and `all` named the old bundles. They meant "everything",
# which is what naming nothing means, so they map to that rather than failing an
# install someone already scripted.
wanted() {
for w in $WANT; do
case $w in
"$1"|"$2"|default|full|all) return 0 ;;
esac
done
return 1
}
# hanzoai/cli/install.sh, fetched once and reused for every tool.
#
# Prefer the API with an explicit raw Accept: it serves the CURRENT file, while
# raw.githubusercontent sits behind a CDN that hands back a copy from some
# minutes ago. Serving a stale script is the exact bug this rewrite removes, so
# do not risk it — but fall back to raw when the API is unreachable or has
# rate-limited an anonymous caller, because a rate limit must not stop an install.
ONE_INSTALLER=''
fetch_one_installer() {
api="https://api.github.com/repos/${CLI_REPO}/contents/install.sh?ref=main"
token="${HANZO_INSTALL_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
# Never interpolate a token into an unquoted argument list; branch instead.
if [ -n "$token" ]; then
curl -fsSL -H "Accept: application/vnd.github.raw" \
-H "Authorization: Bearer $token" "$api" -o "$1" 2>/dev/null && return 0
else
curl -fsSL -H "Accept: application/vnd.github.raw" "$api" -o "$1" 2>/dev/null && return 0
fi
curl -fsSL "https://raw.githubusercontent.com/${CLI_REPO}/main/install.sh" -o "$1" 2>/dev/null
}
# install <repo> <binary> <second-name>
install_tool() {
i_repo="$1"; i_bin="$2"; i_alias="$3"
# HANZO_VERSION names a CLI release. Passing it to anything else would pin a
# tag that repo has never published, so it reaches exactly one tool.
i_pin=''
if [ "$i_repo" = "$CLI_REPO" ]; then i_pin="${HANZO_VERSION:-}"; fi
if i_out=$(HANZO_INSTALL_PREFIX="$HANZO_DIR" \
HANZO_INSTALL_REPO="$i_repo" \
HANZO_INSTALL_BIN="$i_bin" \
HANZO_INSTALL_ALIAS="$i_alias" \
HANZO_VERSION="$i_pin" \
sh "$ONE_INSTALLER" 2>&1); then
# Surface the installer's own warnings (PATH shadowing, PATH missing).
# They are the difference between an install that worked and one the user
# will never actually run — so carry the CONTINUATION lines too. The
# shadow warning's second line is the half that says what to do about it,
# and a filter that keeps only the alarm and drops the remedy is worse
# than not warning at all.
printf '%s\n' "$i_out" \
| grep -E 'WARNING|Remove it, or put|not on PATH|export PATH' \
| sed 's/^/ /' || true
else
printf '%s\n' "$i_out" | sed 's/^/ /'
FAILED_N=$((FAILED_N + 1)); FAILED_LIST="$FAILED_LIST $i_bin"
fail "$i_bin"; return 1
fi
# Trust the artifact, not the log line: ask the binary that is now on disk.
if [ ! -x "$HANZO_DIR/$i_bin" ]; then
FAILED_N=$((FAILED_N + 1)); FAILED_LIST="$FAILED_LIST $i_bin"
fail "$i_bin (nothing at $HANZO_DIR/$i_bin)"; return 1
fi
# The first token that looks like a version, not the last token on the line:
# `--version` output is not one shape across tools (`hanzo 1.9.18` ends on
# the version, `hanzo dev 0.6.91 (sha) 2026-04-01` ends on a date), and
# printing a build date where a version belongs is its own small lie.
i_ver=$("$HANZO_DIR/$i_bin" --version 2>/dev/null | head -1 \
| tr ' ' '\n' | grep -m1 -E '^v?[0-9]+\.[0-9]+' | sed 's/^v//')
[ -n "${i_ver:-}" ] || i_ver='?'
if [ -n "$i_alias" ]; then
# Both names or it is not a finished install. A missing second name falls
# through to whatever older copy is already on the box, silently.
if [ ! -e "$HANZO_DIR/$i_alias" ]; then
FAILED_N=$((FAILED_N + 1)); FAILED_LIST="$FAILED_LIST $i_alias"
fail "$i_alias was not installed alongside $i_bin"; return 1
fi
ok "$i_bin $i_ver ${DM}(+ $i_alias, same build)${N}"
else
ok "$i_bin $i_ver"
fi
INSTALLED_N=$((INSTALLED_N + 1))
}
show_unavailable() {
[ "$HANZO_QUIET" = 1 ] && return 0
printf '\n %snot included:%s\n' "$BD" "$N"
unavailable_rows | while IFS='|' read -r u_name u_why; do
printf ' %s○ %-8s %s%s\n' "$DM" "$u_name" "$u_why" "$N"
done
}
finish() {
printf '\n'
if [ "$INSTALLED_N" -eq 0 ]; then die "nothing was installed"; fi
# Never say "ready" over a failure. A partial install that reads as success
# is how someone spends an afternoon on a tool that was never there.
if [ "$FAILED_N" -gt 0 ]; then
printf ' %s%sincomplete%s — did not install:%s\n' "$R" "$BD" "$N" "$N"
for f in $FAILED_LIST; do printf ' %s✗%s %s\n' "$R" "$N" "$f"; done
printf '\n'
return 0
fi
printf ' %sready%s %s→ %s%s\n\n' "$G" "$N" "$DM" "$HANZO_DIR" "$N"
printf ' quick start:\n'
printf ' %shanzo auth login%s # authenticate\n' "$C" "$N"
printf ' %shanzo code%s # start a coding session\n' "$C" "$N"
printf ' %shanzo --help%s # every command\n' "$C" "$N"
if [ -x "$HANZO_DIR/hanzo-mcp" ]; then
printf ' %shanzo-mcp --help%s # the MCP server\n' "$C" "$N"
fi
printf '\n docs: https://docs.hanzo.ai\n\n'
}
main() {
parse_args "$@"
banner
command -v curl >/dev/null 2>&1 || die "need curl on PATH"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
ONE_INSTALLER="$tmp/install.sh"
fetch_one_installer "$ONE_INSTALLER" \
|| die "could not fetch the installer from ${CLI_REPO}"
[ -s "$ONE_INSTALLER" ] || die "the installer fetched from ${CLI_REPO} is empty"
mkdir -p "$HANZO_DIR"
selected=0
for row in $TOOLS; do
t_name=${row%%:*}; rest=${row#*:}
t_repo=${rest%%:*}; rest=${rest#*:}
t_bin=${rest%%:*}; t_alias=${rest#*:}
if [ -n "$WANT" ] && ! wanted "$t_name" "$t_bin"; then continue; fi
selected=1
install_tool "$t_repo" "$t_bin" "$t_alias" || true
done
if [ "$selected" -eq 0 ]; then
# Asked for something real that we publish no binary for. Answer with the
# reason from the one place that holds it, rather than a "no such tool"
# that reads like a typo. Non-zero: they asked for an install and did not
# get one.
for w in $WANT; do
why=$(unavailable_rows | while IFS='|' read -r u_name u_why; do
if [ "$u_name" = "$w" ]; then printf '%s' "$u_why"; fi
done)
if [ -n "$why" ]; then
fail "$w — $why"
printf ' %sinstallable today:%s%s\n' "$DM" "$(tool_names)" "$N"
exit 1
fi
done
die "no such tool:$WANT — installable today:$(tool_names)"
fi
if [ -z "$WANT" ]; then show_unavailable; fi
finish
# A failed download or a missing second name is a failed install, and the
# exit code has to say so — a half-install discovered days later is worse
# than one that stopped here.
if [ "$FAILED_N" -gt 0 ]; then exit 1; fi
exit 0
}
main "$@"
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env node
/**
* Post-build script to create polyglot index.html
* Makes the site work with: curl hanzo.sh | sh
*/
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const distDir = join(rootDir, 'dist');
const publicDir = join(rootDir, 'public');
// Read the built HTML
const indexPath = join(distDir, 'index.html');
let html = readFileSync(indexPath, 'utf-8');
// Read the install script (skip shebang)
const installScript = readFileSync(join(publicDir, 'install.sh'), 'utf-8');
const scriptContent = installScript.split('\n').slice(1).join('\n');
// Modify HTML closing tag to start HTML comment (hides shell script from browser)
html = html.replace('</html>', '</html><!--');
// Create polyglot file
const polyglot = `#!/bin/sh
<<\\EOF
${html}
EOF
${scriptContent}
`;
writeFileSync(indexPath, polyglot);
console.log('✓ Created polyglot index.html');
console.log(' curl hanzo.sh | sh → runs installer');
console.log(' open hanzo.sh → shows landing page');
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env node
/**
* Two things Vite cannot know about the shape hanzo.sh is served in.
*
* 1. The document must NOT be dist/index.html. Static Assets serve an exact
* match before the Worker ever runs, so a file at that name would be handed
* to `curl hanzo.sh | sh` as HTML. It moves to page.html; worker.js decides
* which of page.html and install.sh `/` means.
* 2. /install is a published alias of /install.sh (some deployed callers ask
* for it). One file in git, two names in dist — copied here rather than
* committed twice, and made from the same bytes so they cannot drift.
*/
import { copyFileSync, renameSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const dist = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist')
renameSync(join(dist, 'index.html'), join(dist, 'page.html'))
copyFileSync(join(dist, 'install.sh'), join(dist, 'install'))
console.log('dist/page.html the document, <!DOCTYPE html> at byte 0')
console.log('dist/install.sh the installer, also served as /install')
console.log('/ whichever of the two the Accept header asks for')
+42
View File
@@ -0,0 +1,42 @@
/**
* hanzo.sh answers one URL with two different things, and always has: the
* landing page to a browser, the installer to `curl -fsSL hanzo.sh | sh` — the
* command the page prints, llms.txt publishes, and every /cli /mcp /dev shim
* re-executes.
*
* It used to do that by shipping a single file that was both at once: the built
* HTML wrapped in a shell heredoc, `#!/bin/sh` on line 1. A browser cannot be
* handed that honestly. Anything before <!DOCTYPE html> that is not whitespace
* or a comment puts the document in quirks mode, and a shebang is neither — so
* compatMode was BackCompat, all fifteen <head> elements (title, icons,
* viewport, every og: and twitter: tag) were parsed into <body> where they do
* nothing, and `#!/bin/sh <<\EOF` was the first text node on the page. No
* arrangement of those bytes avoids it: the shell reads `<` as a redirect
* operator, so a POSIX script cannot begin with `<!`, and only whitespace and
* comments may precede a DOCTYPE. The trick was unfixable, not unlucky.
*
* So: one URL, two representations, chosen by Accept. That is what content
* negotiation is for. curl asks for anything and gets the script; a browser
* asks for text/html and gets a document whose first byte is `<`.
*/
const PAGE = '/page.html'
const INSTALLER = '/install.sh'
export default {
async fetch (request, env) {
const url = new URL(request.url)
if (url.pathname !== '/') return env.ASSETS.fetch(request)
const wantsPage = (request.headers.get('accept') || '').includes('text/html')
const res = await env.ASSETS.fetch(new URL(wantsPage ? PAGE : INSTALLER, url))
// One URL, two answers: no shared cache may keep one and hand it to the
// other kind of client. Cloudflare only honours Vary on Accept-Encoding, so
// say it and also refuse storage outright.
const headers = new Headers(res.headers)
headers.set('vary', 'accept')
headers.set('cache-control', 'no-store')
return new Response(res.body, { status: res.status, headers })
}
}
+25 -9
View File
@@ -1,16 +1,32 @@
# hanzo.sh — assets-only Worker (CF Pages project limit reached; 7stars precedent)
# hanzo.sh — a Worker over static assets (CF Pages project limit reached;
# 7stars precedent).
#
# Deploying is .github/workflows/deploy.yml, which also re-fetches hanzo.sh and
# fails unless the live bytes are the ones it just built. Do not publish this by
# hand: hand-publishing is why the site served an installer months older than
# main. `npx wrangler@3 deploy` still works locally for a break-glass, but then
# main and the live host are two different states again — check afterwards.
# The Worker exists for one rule: `/` is the installer to curl and the landing
# page to a browser (worker.js). Every other path is a plain asset, matched
# exactly — html_handling "none" so nothing is silently rewritten or redirected,
# not_found_handling "none" so a mistyped path 404s instead of being handed an
# installer under a name that promises something else.
#
# Deploying is .github/workflows/deploy.yml, which re-fetches hanzo.sh in both
# representations and fails unless the live bytes are the ones it just built. Do
# not publish this by hand: hand-publishing is why the site served an installer
# months older than main. `npx wrangler@3 deploy` still works locally for a
# break-glass, but then main and the live host are two different states again —
# check afterwards.
name = "hanzo-sh"
main = "worker.js"
compatibility_date = "2026-07-01"
[assets]
directory = "dist"
# Above [assets], not below it: TOML puts every key after a table header inside
# that table, so this used to parse as assets.routes and wrangler dropped it
# with a warning ("Unexpected fields found in assets field"). The custom domain
# survived only because it was attached out of band.
routes = [
{ pattern = "hanzo.sh", custom_domain = true }
]
[assets]
directory = "dist"
binding = "ASSETS"
html_handling = "none"
not_found_handling = "none"