From 7e0c357f7dc5b6917342bb77120bb5d40ba87134 Mon Sep 17 00:00:00 2001 From: Preetham Kyanam <37784174+pkyanam@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:25:31 +0000 Subject: [PATCH 1/2] Publish hstack.sh and install from its archive. Rebase onto the addon installer. install.sh and install.ps1 download the hstack.sh tarball, verify sha256, and keep --with, --dry-run, and --uninstall. The archive includes the skills, apply_addons.py, and the bin helpers. Host the site on Cloudflare Pages. --- .github/workflows/ci.yml | 11 +- README.md | 12 +- install.ps1 | 72 +++- install.sh | 91 ++++- scripts/check_install.sh | 10 + scripts/check_piped_install.sh | 168 ++++++++++ site/.gitignore | 3 + site/DEPLOY.md | 46 +++ site/_headers | 39 +++ site/_redirects | 2 + site/fonts/inter-bold.woff2 | Bin 0 -> 25264 bytes site/fonts/inter-regular.woff2 | Bin 0 -> 24732 bytes site/functions/_middleware.js | 9 + site/functions/install/[...path].js | 9 + site/index.html | 158 +++++++++ site/package-lock.json | 497 ++++++++++++++++++++++++++++ site/package.json | 13 + site/public/404.html | 20 ++ site/public/favicon.svg | 8 + site/scripts/build.mjs | 223 +++++++++++++ site/scripts/check-no-html.mjs | 71 ++++ site/scripts/check-pages.mjs | 135 ++++++++ site/scripts/preview.mjs | 134 ++++++++ site/scripts/shoot.mjs | 120 +++++++ site/src/glass.js | 182 ++++++++++ site/src/main.js | 178 ++++++++++ site/src/styles.css | 462 ++++++++++++++++++++++++++ 27 files changed, 2646 insertions(+), 27 deletions(-) create mode 100755 scripts/check_piped_install.sh create mode 100644 site/.gitignore create mode 100644 site/DEPLOY.md create mode 100644 site/_headers create mode 100644 site/_redirects create mode 100644 site/fonts/inter-bold.woff2 create mode 100644 site/fonts/inter-regular.woff2 create mode 100644 site/functions/_middleware.js create mode 100644 site/functions/install/[...path].js create mode 100644 site/index.html create mode 100644 site/package-lock.json create mode 100644 site/package.json create mode 100644 site/public/404.html create mode 100644 site/public/favicon.svg create mode 100644 site/scripts/build.mjs create mode 100644 site/scripts/check-no-html.mjs create mode 100644 site/scripts/check-pages.mjs create mode 100644 site/scripts/preview.mjs create mode 100644 site/scripts/shoot.mjs create mode 100644 site/src/glass.js create mode 100644 site/src/main.js create mode 100644 site/src/styles.css diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff674d8..a6c740e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,19 @@ jobs: run: | sudo apt-get update sudo apt-get install -y shellcheck - shellcheck -s sh install.sh scripts/check_install.sh + shellcheck -s sh install.sh scripts/check_install.sh scripts/check_piped_install.sh - name: Python syntax run: python3 -m py_compile scripts/check_skills.py scripts/apply_addons.py bin/hstack-bench bin/hstack-test bin/hstack-pr bin/hstack-visdiff bin/hstack-scan bin/hstack-decisions - name: Skills run: python3 scripts/check_skills.py - name: Installer run: sh scripts/check_install.sh + - name: Site + run: | + cd site + npm ci + npm run build + node scripts/check-no-html.mjs + node scripts/check-pages.mjs + cd .. + sh scripts/check_piped_install.sh diff --git a/README.md b/README.md index 60bf8f8..33d6ee3 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,11 @@ Twelve skills for Hermes Agent. Frame the job, shape the change, prove it, and o curl -fsSL https://hstack.sh/install | sh ``` -From a checkout of this repo, `sh install.sh` installs that checkout. A second run replaces the HStack skills and leaves every other skill alone. Pass `--dry-run` to print the plan and write nothing. +The curl form downloads `install.sh` and the skill archive from hstack.sh, checks the sha256, and extracts into the Hermes skills directory. It does not clone this repository. `HSTACK_BASE_URL` points the download at another host, such as a `*.pages.dev` URL before DNS is in place. `HSTACK_VERSION=0.1.0` pins `hstack-v0.1.0.tar.gz` instead of the latest alias. The archive is one directory containing the skills, `scripts/apply_addons.py`, and the `bin/` helpers, so `--with` works from the curl form too. -Skills land in `~/.hermes/skills/hstack/`. Set `HERMES_HOME` if Hermes lives somewhere else. `install.sh` is for macOS and Linux. On Windows, `install.ps1` takes the same flags. WSL can run `install.sh`. +From a checkout of this repo, `sh install.sh` installs the skills beside the script. A second run replaces the HStack skills and leaves every other skill alone. Pass `--dry-run` to print the plan and write nothing. + +Skills land in `~/.hermes/skills/hstack/`. Set `HERMES_HOME` if Hermes lives somewhere else. `install.sh` is for macOS and Linux. On Windows, `https://hstack.sh/install.ps1` takes the same flags and downloads the same archive. WSL can run `install.sh`. ## Why this instead of GStack @@ -35,10 +37,10 @@ GStack's Hermes host puts every skill in the always-on index. HStack puts one ro | | GStack on Hermes | HStack | |---|---:|---:| | Idle index | 1,182 | 204 | -| Planning load | 62,514 of SKILL.md, plus a ~5,600 preamble | 2,500 | -| All SKILL.md bodies | 420,401 | 8,550 | +| Planning load | 62,514 of SKILL.md, plus a ~5,600 preamble | 965 | +| All SKILL.md bodies | 420,401 | 3,820 | -The planning path is `hstack`, `reframe`, and `shape`. 2,500 against 62,514 is about 25× smaller. The idle index is about 6× smaller. HStack's body figures are the v0.1 ceilings. 204 is the index cost of these descriptions, including the `hstack` category header. +The planning path is `hstack`, `reframe`, and `shape`. 965 against 62,514 is under 2% of that context. The idle index is about 6× smaller. These HStack figures are measured from the files in this repo. 204 is the index cost of these descriptions, including the `hstack` category header. The v0.1 ceilings in PLAN.md are 2,500 and 8,550. ## Skills diff --git a/install.ps1 b/install.ps1 index 5d54461..0a8dc52 100644 --- a/install.ps1 +++ b/install.ps1 @@ -27,7 +27,8 @@ Usage: powershell -File install.ps1 [--dry-run] [--uninstall] [--with name,name] Installs into the Hermes home, at skills\hstack. The usual home is .hermes in your profile directory. The default install is skills only. --with adds an optional addon: browser, github, docs, search, perf, test, security, trail. -HSTACK_REF selects the git branch when the script downloads (default: main). +The download form fetches the archive from hstack.sh. +HSTACK_BASE_URL overrides that host. HSTACK_VERSION pins a release. Running this file from a git checkout installs that checkout. '@ } @@ -198,13 +199,23 @@ if ((Test-Path -LiteralPath $Dest) -and -not (Test-Path -LiteralPath $Marker)) { } } +$Url = '' +$Sum = '' if (-not $Mode) { $Mode = 'remote' - $Ref = if ($env:HSTACK_REF) { $env:HSTACK_REF } else { 'main' } - if ($Ref -notmatch '^[A-Za-z0-9._-]+$' -or $Ref.StartsWith('-') -or $Ref.Contains('..')) { - Die 'refusing HSTACK_REF' + $Base = if ($env:HSTACK_BASE_URL) { $env:HSTACK_BASE_URL.TrimEnd('/') } else { 'https://hstack.sh' } + if ($Base -notmatch '^https?://' -or $Base -match '\s|<|>|\.\.') { + Die 'refusing HSTACK_BASE_URL' } - $Url = "https://github.com/Belweave/hstack/archive/refs/heads/$Ref.tar.gz" + $Name = 'hstack.tar.gz' + if ($env:HSTACK_VERSION) { + if ($env:HSTACK_VERSION -notmatch '^[0-9.]+$' -or $env:HSTACK_VERSION.Contains('..')) { + Die 'refusing HSTACK_VERSION' + } + $Name = "hstack-v$($env:HSTACK_VERSION).tar.gz" + } + $Url = "$Base/dl/$Name" + $Sum = "$Base/dl/$Name.sha256" } if ($Dry) { @@ -213,6 +224,7 @@ if ($Dry) { Say "dry-run: source tree $Src" } else { Say "dry-run: would download $Url" + Say "dry-run: would verify $Sum" } Invoke-Addons 'install' exit 0 @@ -221,9 +233,33 @@ if ($Dry) { if ($Mode -eq 'remote') { $Work = Join-Path ([System.IO.Path]::GetTempPath()) ("hstack-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $Work | Out-Null + $RemoteOk = $false try { $archive = Join-Path $Work 'hstack.tar.gz' - Invoke-WebRequest -Uri $Url -OutFile $archive + $sumfile = Join-Path $Work 'hstack.tar.gz.sha256' + Invoke-WebRequest -Uri $Url -OutFile $archive -UseBasicParsing + Invoke-WebRequest -Uri $Sum -OutFile $sumfile -UseBasicParsing + if ((Get-Item -LiteralPath $archive).Length -lt 1 -or (Get-Item -LiteralPath $sumfile).Length -lt 1) { + Die 'download was empty' + } + $stream = [System.IO.File]::OpenRead($archive) + try { + $b0 = $stream.ReadByte() + $b1 = $stream.ReadByte() + } finally { + $stream.Dispose() + } + if ($b0 -ne 0x1f -or $b1 -ne 0x8b) { + Die 'download was not a gzip archive' + } + $expected = ((Get-Content -LiteralPath $sumfile -TotalCount 1) -split '\s+', 2)[0] + if ($expected -notmatch '^[0-9A-Fa-f]{64}$') { + Die 'checksum was not a sha256 digest' + } + $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash + if ($actual.ToLowerInvariant() -ne $expected.ToLowerInvariant()) { + Die 'checksum mismatch' + } $members = & tar -tzf $archive if ($LASTEXITCODE -ne 0) { Die 'could not read archive' } foreach ($member in $members) { @@ -231,6 +267,13 @@ if ($Mode -eq 'remote') { Die 'archive member is absolute or contains ..' } } + $listing = & tar -tvzf $archive + if ($LASTEXITCODE -ne 0) { Die 'could not list archive' } + foreach ($line in $listing) { + if ($line.StartsWith('l') -or $line.StartsWith('h')) { + Die 'archive contains a link; refusing to extract it' + } + } & tar -xzf $archive -C $Work if ($LASTEXITCODE -ne 0) { Die 'could not extract archive' } $tops = @(Get-ChildItem -LiteralPath $Work -Directory) @@ -240,8 +283,9 @@ if ($Mode -eq 'remote') { if (-not (Test-Path -LiteralPath (Join-Path $Src 'hstack\SKILL.md'))) { Die 'archive is missing skills/hstack/hstack/SKILL.md' } + $RemoteOk = $true } finally { - if ($Mode -eq 'remote' -and (Test-Path -LiteralPath $Work) -and -not $Src) { + if (-not $RemoteOk -and (Test-Path -LiteralPath $Work)) { Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue } } @@ -255,6 +299,12 @@ if ($links.Count -gt 0) { Die 'refusing to copy a link or special file from the skill tree' } +$ver = '0.1.0' +$versionFile = if ($AddonRoot) { Join-Path $AddonRoot 'VERSION' } else { $null } +if ($versionFile -and (Test-Path -LiteralPath $versionFile)) { + $digits = ([System.IO.File]::ReadAllText($versionFile) -replace '[^0-9.]', '') + if ($digits) { $ver = $digits } +} $Parent = Split-Path -Parent $Dest if (-not (Test-Path -LiteralPath $Parent)) { New-Item -ItemType Directory -Path $Parent | Out-Null @@ -262,10 +312,16 @@ if (-not (Test-Path -LiteralPath $Parent)) { $Stage = Join-Path $Parent ('.hstack-stage.' + [guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Path $Stage | Out-Null Copy-Item -Path (Join-Path $Src '*') -Destination $Stage -Recurse -Force +$stagedVersion = Join-Path $Stage 'VERSION' +if (Test-Path -LiteralPath $stagedVersion) { + $digits = ([System.IO.File]::ReadAllText($stagedVersion) -replace '[^0-9.]', '') + if ($digits) { $ver = $digits } + Remove-Item -LiteralPath $stagedVersion -Force +} if (-not (Test-Path -LiteralPath (Join-Path $Stage 'hstack\SKILL.md'))) { Die 'install tree is missing hstack/SKILL.md' } -Set-Content -LiteralPath (Join-Path $Stage '.hstack-managed') -Value "managed-by: hstack install.sh`nversion: 0.1.0`n" -NoNewline -Encoding ascii +Set-Content -LiteralPath (Join-Path $Stage '.hstack-managed') -Value "managed-by: hstack install.sh`nversion: $ver`n" -NoNewline -Encoding ascii if (Test-Path -LiteralPath $Dest) { $Prev = Join-Path $Parent ('.hstack-prev.' + $PID) diff --git a/install.sh b/install.sh index 5ba1981..896c766 100755 --- a/install.sh +++ b/install.sh @@ -2,10 +2,13 @@ # Install HStack into Hermes Agent. # curl -fsSL https://hstack.sh/install | sh # curl -fsSL https://hstack.sh/install | sh -s -- --uninstall -# sh install.sh [--dry-run] [--uninstall] +# sh install.sh [--dry-run] [--uninstall] [--with name,name] # Skills go to ${HERMES_HOME:-$HOME/.hermes}/skills/hstack. # A second run replaces an install this script created. It will not touch # any other skill, and it will not delete a directory it did not mark. +# The curl form downloads the archive from hstack.sh. The repository is private, +# so this script does not fetch from a git host. The archive also carries the +# addon helpers under bin/ and scripts/apply_addons.py. set -eu @@ -25,7 +28,8 @@ usage() { 'Installs into the Hermes home, at skills/hstack. The usual home is .hermes in your home directory.' \ 'The default install is skills only. --with adds an optional addon:' \ 'browser, github, docs, search, perf, test, security, trail.' \ - 'HSTACK_REF selects the git branch when the script downloads (default: main).' \ + 'The curl form downloads the archive from hstack.sh.' \ + 'HSTACK_BASE_URL overrides that host. HSTACK_VERSION pins a release.' \ 'Running this file from a git checkout installs that checkout.' } @@ -251,6 +255,7 @@ fi MODE= SRC= URL= +SUM= if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/skills/hstack/hstack/SKILL.md" ]; then MODE=local SRC="$SCRIPT_DIR/skills/hstack" @@ -259,14 +264,26 @@ fi if [ -z "$MODE" ]; then MODE=remote - REF=${HSTACK_REF:-main} - case "$REF" in - ''|-*|*..*|*[[:space:]]*|*'/'*) die "refusing HSTACK_REF" ;; + BASE=${HSTACK_BASE_URL:-https://hstack.sh} + case "$BASE" in + */) BASE=${BASE%/} ;; esac - case "$REF" in - *[!A-Za-z0-9._-]*) die "refusing HSTACK_REF" ;; + case "$BASE" in + http://*|https://*) ;; + *) die "refusing HSTACK_BASE_URL" ;; esac - URL="https://github.com/Belweave/hstack/archive/refs/heads/${REF}.tar.gz" + case "$BASE" in + *[[:space:]]*|*'<'*|*'..'*) die "refusing HSTACK_BASE_URL" ;; + esac + NAME=hstack.tar.gz + if [ -n "${HSTACK_VERSION:-}" ]; then + case "$HSTACK_VERSION" in + ''|*[!0-9.]*|*..*) die "refusing HSTACK_VERSION" ;; + esac + NAME="hstack-v${HSTACK_VERSION}.tar.gz" + fi + URL="$BASE/dl/$NAME" + SUM="$BASE/dl/${NAME}.sha256" fi if [ "$DRY" -eq 1 ]; then @@ -275,23 +292,57 @@ if [ "$DRY" -eq 1 ]; then say "dry-run: source tree $SRC" else say "dry-run: would download $URL" + say "dry-run: would verify $SUM" fi run_addons install exit 0 fi +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk 'NR==1 { print $1 }' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk 'NR==1 { print $1 }' + else + die "sha256sum or shasum is required" + fi +} + if [ "$MODE" = remote ]; then command -v curl >/dev/null 2>&1 || die "curl is required to download HStack" command -v tar >/dev/null 2>&1 || die "tar is required to download HStack" WORK=$(mktemp -d) archive="$WORK/hstack.tar.gz" + sumfile="$WORK/hstack.tar.gz.sha256" curl -fsSL -o "$archive" "$URL" || die "download failed" - if [ ! -s "$archive" ]; then + curl -fsSL -o "$sumfile" "$SUM" || die "checksum download failed" + if [ ! -s "$archive" ] || [ ! -s "$sumfile" ]; then die "download was empty" fi + magic=$(od -An -t x1 -N 2 "$archive" | tr -d ' \n') + case "$magic" in + 1f8b) ;; + *) die "download was not a gzip archive" ;; + esac + expected=$(awk 'NR==1 { print $1 }' "$sumfile") + case "$expected" in + *[!0-9A-Fa-f]*|'') die "checksum was not a sha256 digest" ;; + esac + elen=$(printf '%s' "$expected" | wc -c | tr -d ' ') + if [ "$elen" -ne 64 ]; then + die "checksum was not a sha256 digest" + fi + actual=$(hash_file "$archive") + if [ "$actual" != "$expected" ]; then + die "checksum mismatch" + fi tar -tzf "$archive" > "$WORK/members" || die "could not read archive" while IFS= read -r member || [ -n "$member" ]; do - case "$member" in + norm=$member + case "$norm" in + ./*) norm=${norm#./} ;; + esac + case "$norm" in ''|/*|*..*) die "archive member is absolute or contains .." ;; esac done < "$WORK/members" @@ -316,26 +367,40 @@ if [ "$MODE" = remote ]; then SRC="$top/skills/hstack" ADDON_ROOT=$top if [ ! -f "$SRC/hstack/SKILL.md" ]; then - die "archive is missing skills/hstack/hstack/SKILL.md" + die "archive is missing hstack/SKILL.md" fi fi if [ ! -f "$SRC/hstack/SKILL.md" ]; then - die "missing skills/hstack/hstack/SKILL.md" + die "missing hstack/SKILL.md" fi if find "$SRC" \( -type l -o -type b -o -type c -o -type p -o -type s \) -print | grep -q .; then die "refusing to copy a link or special file from the skill tree" fi +ver=0.1.0 +if [ -n "$ADDON_ROOT" ] && [ -f "$ADDON_ROOT/VERSION" ]; then + got=$(tr -cd '0-9.' < "$ADDON_ROOT/VERSION") + if [ -n "$got" ]; then + ver=$got + fi +fi parent=$(dirname "$DEST") mkdir -p "$parent" STAGE=$(mktemp -d "${parent}/.hstack-stage.XXXXXX") cp -R "$SRC/." "$STAGE/" +if [ -f "$STAGE/VERSION" ]; then + got=$(tr -cd '0-9.' < "$STAGE/VERSION") + if [ -n "$got" ]; then + ver=$got + fi + rm -f "$STAGE/VERSION" +fi if [ ! -f "$STAGE/hstack/SKILL.md" ]; then die "install tree is missing hstack/SKILL.md" fi -printf '%s\n' 'managed-by: hstack install.sh' 'version: 0.1.0' > "$STAGE/.hstack-managed" +printf '%s\n' 'managed-by: hstack install.sh' "version: $ver" > "$STAGE/.hstack-managed" if [ -d "$DEST" ]; then PREV="${parent}/.hstack-prev.$$" diff --git a/scripts/check_install.sh b/scripts/check_install.sh index a340757..8851628 100755 --- a/scripts/check_install.sh +++ b/scripts/check_install.sh @@ -12,6 +12,16 @@ fail() { exit 1 } +if grep -q 'github.com' "$INSTALL"; then + fail "installer must not fetch from GitHub" +fi +if grep -q 'github.com' "$ROOT/install.ps1"; then + fail "install.ps1 must not fetch from GitHub" +fi +if grep -q 'HSTACK_REF' "$INSTALL" "$ROOT/install.ps1"; then + fail "installers still select a git ref" +fi + count_skills() { _n=0 for _dir in "$1"/skills/hstack/*; do diff --git a/scripts/check_piped_install.sh b/scripts/check_piped_install.sh new file mode 100755 index 0000000..b77f504 --- /dev/null +++ b/scripts/check_piped_install.sh @@ -0,0 +1,168 @@ +#!/bin/sh +# Piped install against the local site build, with GNU tar and with BSD tar plus shasum. +set -eu + +ROOT=$(CDPATH='' cd "$(dirname "$0")/.." && pwd -P) +SITE=$ROOT/site +TMP=$(mktemp -d) +PORT=${PORT:-4179} +PID= + +fail() { + printf 'check_piped: %s\n' "$1" >&2 + exit 1 +} + +cleanup() { + if [ -n "$PID" ]; then + kill "$PID" >/dev/null 2>&1 || true + fi + rm -rf "$TMP" +} +trap cleanup EXIT + +if [ ! -f "$SITE/dist/install.sh" ] || [ ! -f "$SITE/dist/dl/hstack.tar.gz" ]; then + fail "site/dist is missing; run npm run build in site/" +fi + +PORT=$PORT node "$SITE/scripts/preview.mjs" >"$TMP/preview.log" 2>&1 & +PID=$! +i=0 +while ! curl -fsS "http://127.0.0.1:$PORT/install" >/dev/null 2>&1; do + i=$((i + 1)) + if [ "$i" -gt 50 ]; then + fail "preview did not start" + fi + sleep 0.1 +done + +count_skills() { + _n=0 + for _dir in "$1"/skills/hstack/*; do + if [ -f "$_dir/SKILL.md" ]; then + _n=$((_n + 1)) + fi + done + printf '%s\n' "$_n" +} + +install_with() { + label=$1 + path_value=$2 + home=$TMP/$label + mkdir -p "$home" + if [ -n "$path_value" ]; then + PATH="$path_value" HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' + else + HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' + fi + test -f "$home/hermes/skills/hstack/.hstack-managed" || fail "$label missing marker" + test -f "$home/hermes/skills/hstack/hstack/SKILL.md" || fail "$label missing router" + test -f "$home/hermes/skills/hstack/hstack/references/frame.md" || fail "$label missing playbook" + test "$(count_skills "$home/hermes")" -eq 12 || fail "$label expected 12 skills" + test ! -e "$home/hermes/skills/hstack/VERSION" || fail "$label left VERSION in the skill tree" + grep -q 'version: 0.1.0' "$home/hermes/skills/hstack/.hstack-managed" || fail "$label marker version" + diff -q "$ROOT/skills/hstack/reframe/SKILL.md" "$home/hermes/skills/hstack/reframe/SKILL.md" >/dev/null \ + || fail "$label skill bytes differ" + if [ -n "$path_value" ]; then + PATH="$path_value" HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --dry-run' >"$TMP/$label.dry" + PATH="$path_value" HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --uninstall' + else + HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --dry-run' >"$TMP/$label.dry" + HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --uninstall' + fi + grep -q "http://127.0.0.1:$PORT/dl/hstack.tar.gz" "$TMP/$label.dry" || fail "$label dry-run omitted the archive URL" + test ! -d "$home/hermes/skills/hstack" || fail "$label uninstall left the tree" +} + +install_with linux "" + +WITH=$TMP/with +mkdir -p "$WITH" +HOME="$WITH" HERMES_HOME="$WITH/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --with browser' +test -x "$WITH/hermes/hstack/bin/hstack-visdiff" || fail "piped --with browser missing visdiff" +test -f "$WITH/hermes/hstack/apply_addons.py" || fail "piped --with browser missing apply_addons.py" +grep -q 'hstack_playwright' "$WITH/hermes/config.yaml" || fail "piped --with browser missing playwright" +test -f "$WITH/hermes/skills/hstack/hstack/SKILL.md" || fail "piped --with browser missing skills" +test ! -e "$WITH/hermes/skills/hstack/bin" || fail "helpers landed in the skill tree" +test ! -e "$WITH/hermes/skills/hstack/VERSION" || fail "piped --with browser left VERSION in the skill tree" + +MAC=$TMP/macbin +mkdir -p "$MAC" +link() { + command -v "$1" >/dev/null 2>&1 || fail "missing $1" + ln -sf "$(command -v "$1")" "$MAC/$2" +} +link curl curl +link shasum shasum +link awk awk +link mktemp mktemp +link find find +link grep grep +link od od +link tr tr +link wc wc +link cp cp +link mv mv +link rm rm +link mkdir mkdir +link dirname dirname +link basename basename +link pwd pwd +ln -sf "$(command -v bsdtar)" "$MAC/tar" +ln -sf /bin/sh "$MAC/sh" +if [ -x "$MAC/sha256sum" ]; then + fail "macOS-like PATH still has sha256sum" +fi +install_with macos "$MAC:/bin" + +HOME=$TMP/pin HERMES_HOME=$TMP/pin/hermes HSTACK_BASE_URL="http://127.0.0.1:$PORT" HSTACK_VERSION=0.1.0 \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' +test -f "$TMP/pin/hermes/skills/hstack/shape/SKILL.md" || fail "version pin did not install" + +mkdir -p "$TMP/bad/dl" +cp "$SITE/dist/dl/hstack.tar.gz" "$TMP/bad/dl/hstack.tar.gz" +printf '%s\n' '0000000000000000000000000000000000000000000000000000000000000000 hstack.tar.gz' > "$TMP/bad/dl/hstack.tar.gz.sha256" +cp "$SITE/dist/install.sh" "$TMP/bad/install.sh" +BADPORT=$((PORT + 1)) +python3 - "$TMP/bad" "$BADPORT" << 'PY' & +import http.server, socketserver, sys +root, port = sys.argv[1], int(sys.argv[2]) +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=root, **kwargs) + def log_message(self, fmt, *args): + return + def do_GET(self): + path = self.path.split("?", 1)[0] + if path in ("/install", "/install/"): + self.path = "/install.sh" + return super().do_GET() +with socketserver.TCPServer(("127.0.0.1", port), Handler) as httpd: + httpd.serve_forever() +PY +BPID=$! +trap 'kill "$PID" "$BPID" >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT +j=0 +while ! curl -fsS "http://127.0.0.1:$BADPORT/install" >/dev/null 2>&1; do + j=$((j + 1)) + if [ "$j" -gt 50 ]; then + fail "bad checksum server did not start" + fi + sleep 0.1 +done +if HOME=$TMP/badhome HERMES_HOME=$TMP/badhome/hermes HSTACK_BASE_URL="http://127.0.0.1:$BADPORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' >"$TMP/bad.out" 2>"$TMP/bad.err"; then + fail "bad checksum should fail" +fi +grep -q 'checksum mismatch' "$TMP/bad.err" || fail "bad checksum was not reported" +test ! -d "$TMP/badhome/hermes/skills/hstack" || fail "bad checksum wrote a skill tree" + +printf '%s\n' 'check_piped: linux, addons, macos-like, version pin, and checksum mismatch ok' diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..9748504 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +src/data.gen.js diff --git a/site/DEPLOY.md b/site/DEPLOY.md new file mode 100644 index 0000000..58b03ff --- /dev/null +++ b/site/DEPLOY.md @@ -0,0 +1,46 @@ +# Deploy + +Production target is Cloudflare Pages on the belweave account, Free plan. Do not deploy from this note; the project owner runs the deploy. + +## Pages project + +Connect the GitHub repository. The site lives in `site/`. + +| Setting | Value | +|---|---| +| Account | belweave | +| Plan | Free | +| Root directory | `site` | +| Framework preset | None | +| Build command | `npm run build` | +| Output directory | `dist` | + +Pages installs from `site/package-lock.json` before the build command. There is no framework preset and no `vercel.json`. + +`site/_headers` and `site/_redirects` are copied into `dist/` by the build. Pages reads those files from the output directory and does not serve them as assets. `site/functions/` stays in the project root. Pages compiles it from there; it is not part of `dist/`. + +## hstack.sh + +The custom domain is `hstack.sh`. DNS for that name is at Porkbun today. The registrar stays Porkbun. + +Pages will not activate an apex custom domain while the DNS stays outside Cloudflare. The supported path is: + +1. Add the `hstack.sh` zone on the belweave Cloudflare account (the zone itself is free). +2. At Porkbun, set the nameservers for `hstack.sh` to the pair Cloudflare shows for that zone. Do not move the registration. +3. In the Pages project, add the custom domain `hstack.sh`. Cloudflare then creates the CNAME it expects. +4. Leave Always Use HTTPS on. It is the default for an active custom domain, and it is what turns `http://hstack.sh/install` into `https://hstack.sh/install` before the script is returned. + +Before the zone is on Cloudflare, the pre-DNS host is the `pages.dev` hostname. A project named `hstack` is `hstack.pages.dev`. `HSTACK_BASE_URL` can point the installer at that host. + +If the records must stay at Porkbun, the record Pages documents for a hostname outside the apex is a CNAME to `.pages.dev`. Porkbun can also store an ALIAS at `@` for that same target, but the Pages dashboard will not mark an external-DNS apex active until the zone is on the belweave account. Do not add an AAAA record. Add the domain in the Pages project before the record exists, or the edge answers 522. + +## What the deploy serves + +- `https://hstack.sh/install` and `https://hstack.sh/install/` are `install.sh`, `Content-Type: text/plain; charset=utf-8`, with or without the trailing slash. +- `https://hstack.sh/install.ps1` is the Windows installer, also `text/plain`. +- Any other path under `/install/` is a plain-text 404 (`not found`), not an HTML page. That response comes from `site/functions/install/[...path].js`. Pages does not honor a 404 status in `_redirects`. +- `curl -fsSL hstack.sh/install` follows the HTTP to HTTPS redirect and receives the script. The body is never an HTML page, including when the path under `/install/` does not exist. +- `https://hstack.sh/dl/hstack.tar.gz` is the latest archive. `hstack.tar.gz.sha256` is its digest. +- `https://hstack.sh/dl/hstack-v0.1.0.tar.gz` is that same release by version. The installer checks the digest before it extracts. + +The archive has one top-level directory. Inside it are `skills/hstack/`, `scripts/apply_addons.py`, `bin/` (the addon helpers), and `VERSION`. diff --git a/site/_headers b/site/_headers new file mode 100644 index 0000000..6f297a1 --- /dev/null +++ b/site/_headers @@ -0,0 +1,39 @@ +/install/* + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + +/install + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/install/ + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/install.sh + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/install.ps1 + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/dl/hstack.tar.gz + Content-Type: application/gzip + Cache-Control: public, max-age=300 + +/dl/hstack.tar.gz.sha256 + Content-Type: text/plain; charset=utf-8 + Cache-Control: public, max-age=300 + +/dl/hstack-v0.1.0.tar.gz + Content-Type: application/gzip + Cache-Control: public, max-age=31536000, immutable + +/dl/hstack-v0.1.0.tar.gz.sha256 + Content-Type: text/plain; charset=utf-8 + Cache-Control: public, max-age=31536000, immutable diff --git a/site/_redirects b/site/_redirects new file mode 100644 index 0000000..7fd46ce --- /dev/null +++ b/site/_redirects @@ -0,0 +1,2 @@ +/install /install.sh 200 +/install/ /install.sh 200 diff --git a/site/fonts/inter-bold.woff2 b/site/fonts/inter-bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e69189c08f23c2cc37b8043a5cb0d67a52860162 GIT binary patch literal 25264 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 H&}#z#+vps{ literal 0 HcmV?d00001 diff --git a/site/fonts/inter-regular.woff2 b/site/fonts/inter-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9348abcd6101ad411ab6f33bf7eade756898ed48 GIT binary patch literal 24732 zcmV)TK(W7fPew8T0RR910AQQ|5dZ)H0RT7v0AM=+1OP$+00000000000000000000 z0000QY8(7o9JX`@U;u_h2!aF&oD2~N3WAI@f$b*&HUcCAi*O5tdH@6<1%h}7APj+T z8;k5Gn43B(sNZb^Du{*h?QdMdrUSn;JvSPipr$yp12H%cL|w4|L)riTpO{o+%%(un zuEW7`UR8~rB$uL)rU=VuO;M##Dhets18I`icX?qu?!D?(Ff2{pA(JCJ|1hG%1yNdX z-*;!yZCtBXDfe+04@kJ%04?};$qiiYZ5G5D$@Ne7$@Y3*1KM_n4G8#b?mN{@nLw z*^Dv9ShgVsW6731jWt@Nq1Doc#`2{xCXFS@M_RrlUqeWiX%pr9LZ$X^%bz4^6Mbpf zwS2!1{D15Xcg}rfH?8PQRJ!n``MAVYG{z!4M&Up>EW#3D!S1&{)$FUPyJrRBV%53u_^Kv8L4qP7*{0mCc3xVU^L7n;#tg%f0obOP6Ejw>tQm!oL!tx= z3nWZLL~4g9;Ia0c;&L{nhf> z(r=f&?e+XivXVYOeaHu}1%UtgF_Qw}pJ-er>;-iXD#gtSE9NpN? z?D#FX6xBZcSC;R%2=5qb~sGON~;%};}KxR+W5M!p)f>Z)Al7H8Y4+ONy7K;P;r9;kRS@CVH&1k8m8Ug z1SATgVH&1k8m8eW`2Tb3eU}gz?o9|8epO`EK8egY_?RSl-TK3$w!dsQ-U!rp+8}QbHkX?4c@= zvbov6`7g6nVDNpN{X6%=cU&M821xrVQzNrgktYkrsq(yba&thBux?*3h4E!Tiz7vLCUm0DMBDmSgW+AbQGjs@U5k`O@U}0XO5%&GCEFARC z`MV?u_!s;kHWB!jj?!e{BLJp}(7GkaOMa0g1Je|^+bT~)If-3D!b3GN4I5$xEV;g3 zu{7m5d`0ow z$UEXsPJNk-H^4feVA6v1Rd;m-S0=jocBLT&9g@U8(yZDAzX@aOle~!#YTA`HN}( z-iv-DpiskskX8>c%~@pOi(yej4V=?;;wux~e7pWJY~i{PK>0px&5L<#WwSV@cyykT5TbG9ce8-%40vok|QKdG0q zwpJ#mm8`#lGr%H@w2&N?sxbkNrMBn*`g3nU@xt3J0Z ziFF=>cpWd;*^PC&hUa#-)MtQ|!T)WjJYZ|o7$*~6@$l9(AK(43#DD&?%>QOB4|A

fz++Y<6jA-OUz+-nItN=nFC!kvNh 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 + + + + + HStack · Everything Hermes needs + + + + + + + + + + + + + + + + + + + + +

+ + + HStack + + + GitHub +
+
+
+
+
+

Everything Hermes needs. Nothing it doesn't.

+
+ curl -fsSL https://hstack.sh/install | sh + +
+
    +
  • MIT
  • +
  • Designed for Hermes Agent
  • +
  • 12 skills
  • +
  • ~204 idle tokens
  • +
+
+ +
+

+
+
+
+

HStack
planning load

+

965tokens

+
+
+ +
+

GStack
planning load

+

62,514tokens

+
+
+

Under 2% of the context. Built for Hermes.

+

A community project. Not affiliated with Nous Research.

+
+
+
+ +
+

The index stays small.

+

Hermes reads every skill description on each turn. HStack keeps one short router there. A playbook loads only when the task needs it. Counts use Hermes's estimator, ceil(bytes / 4).

+ +
+ +
+

Twelve skills.

+

One line in the index. The body loads when you ask for that skill.

+
+
+ +
+

One router. One playbook.

+

/hstack is the skill to remember. It opens the route catalog and loads one playbook. Everything else stays on disk.

+
    +
  1. frameDeciding what to build. Load reframe, then stop or hand off.
  2. +
  3. featureNew or changed behavior. Shape it, implement it, prove it.
  4. +
  5. fixSomething is broken. Use systematic-debugging, then prove the fix.
  6. +
  7. checkThe diff needs a hard look. Council only if you asked for other models.
  8. +
  9. landThe work should become a pull request. Load ship.
  10. +
+

A one-line edit does not need a playbook. If you name another HStack skill, that skill loads and the router steps aside.

+
+ +
+

Use it in the session.

+

Hermes lists the skills under hstack. Ask with the skill name.

+
/hstack the export drops rows over 10 MB
+/reframe
+/ship
+/council
+
+ +
+

Install

+

macOS and Linux. The script is safe to run twice. It only replaces a tree it created.

+
+
+
+

Install

+ +
+
curl -fsSL https://hstack.sh/install | sh
+
+
+
+

Dry run

+ +
+
curl -fsSL https://hstack.sh/install | sh -s -- --dry-run
+
+
+
+

Uninstall

+ +
+
curl -fsSL https://hstack.sh/install | sh -s -- --uninstall
+
+
+

From a checkout of the repo, sh install.sh installs the skills next to the script. HSTACK_BASE_URL points the curl form at another host, which is how you try a *.pages.dev URL before DNS. Optional add-ons stay off until you pass --with.

+
+
+
+

HStack is a community project made for the Hermes Agent community. It is not affiliated with, endorsed by, or an official product of Nous Research.

+

belweave/hstack

+
+ + + diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..0d07097 --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,497 @@ +{ + "name": "hstack-site", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hstack-site", + "devDependencies": { + "esbuild": "0.25.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..c27e86f --- /dev/null +++ b/site/package.json @@ -0,0 +1,13 @@ +{ + "name": "hstack-site", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "preview": "node scripts/preview.mjs", + "check": "node scripts/check-no-html.mjs && node scripts/check-pages.mjs" + }, + "devDependencies": { + "esbuild": "0.25.10" + } +} diff --git a/site/public/404.html b/site/public/404.html new file mode 100644 index 0000000..4a84425 --- /dev/null +++ b/site/public/404.html @@ -0,0 +1,20 @@ + + + + + + Not found · HStack + + + + +
+

That page is not here.

+

Back to HStack

+
+ + diff --git a/site/public/favicon.svg b/site/public/favicon.svg new file mode 100644 index 0000000..f0a64b1 --- /dev/null +++ b/site/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/site/scripts/build.mjs b/site/scripts/build.mjs new file mode 100644 index 0000000..d30583d --- /dev/null +++ b/site/scripts/build.mjs @@ -0,0 +1,223 @@ +import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, copyFileSync } from "node:fs"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import crypto from "node:crypto"; +import zlib from "node:zlib"; +import esbuild from "esbuild"; + +const site = path.resolve(import.meta.dirname, ".."); +const root = path.resolve(site, ".."); +const dist = path.join(site, "dist"); +const skillsRoot = path.join(root, "skills", "hstack"); + +const ORDER = [ + "hstack", + "reframe", + "shape", + "prove", + "council", + "ship", + "takeover", + "modernize", + "blast", + "swarm", + "arena", + "principles", +]; + +const GSTACK = { idle: 1182, planning: 62514, bodies: 420401 }; + +function tokens(text, label) { + const bytes = Buffer.byteLength(text, "utf8"); + if (bytes !== text.length) throw new Error(`${label} is not ASCII`); + return Math.ceil(bytes / 4); +} + +function field(md, key, file) { + const match = md.match(new RegExp(`^${key}:\\s*(.*)$`, "m")); + if (!match || !match[1].trim()) throw new Error(`${file} is missing ${key}`); + return match[1].trim(); +} + +function fmt(value) { + return new Intl.NumberFormat("en-US").format(value); +} + +function esc(value) { + return value.replace(/[&<>"']/g, (ch) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[ch])); +} + +const skills = ORDER.map((name) => { + const file = path.join(skillsRoot, name, "SKILL.md"); + const md = readFileSync(file, "utf8"); + const description = field(md, "description", file); + const version = field(md, "version", file); + const row = ` - ${name}: ${description}`; + return { + name, + description, + version, + idle: tokens(row, row), + body: tokens(md, file), + }; +}); + +const version = skills[0].version; +if (!skills.every((skill) => skill.version === version)) { + throw new Error("skill versions differ"); +} +if (version !== "0.1.0") { + throw new Error(`version is ${version}; dist publishes hstack-v0.1.0.tar.gz`); +} + +const idle = tokens(" hstack:", "category") + skills.reduce((sum, skill) => sum + skill.idle, 0); +const planning = ["hstack", "reframe", "shape"].reduce((sum, name) => { + return sum + skills.find((skill) => skill.name === name).body; +}, 0); +const bodies = skills.reduce((sum, skill) => sum + skill.body, 0); + +if (idle !== 204 || planning !== 965 || bodies !== 3820) { + throw new Error(`measured idle ${idle}, planning ${planning}, bodies ${bodies}`); +} + +const stack = skills.map((skill) => { + const label = `${skill.name}. ${skill.description} ${skill.idle} idle tokens, ${fmt(skill.body)} body tokens.`; + return `

${esc(skill.description)}

`; +}).join(""); + +const grid = skills.map((skill) => { + return `

${esc(skill.name)}

${esc(skill.description)}

${skill.idle} idle · ${fmt(skill.body)} body

`; +}).join(""); + +function meter(title, hstack, gstack) { + const share = (hstack / gstack).toFixed(6); + return `

${title}

HStack
${fmt(hstack)}
GStack
${fmt(gstack)}
`; +} + +const compare = `
${meter("Idle, every turn", idle, GSTACK.idle)}${meter("Planning load", planning, GSTACK.planning)}${meter("All skill bodies", bodies, GSTACK.bodies)}
`; + +const css = await esbuild.transform(readFileSync(path.join(site, "src", "styles.css"), "utf8"), { + loader: "css", + minify: true, +}); + +let html = readFileSync(path.join(site, "index.html"), "utf8"); +html = html.replace("/*__CSS__*/", () => css.code); +html = html.replace("", () => stack); +html = html.replace("", () => grid); +html = html.replace("", () => compare); +if (html.includes("__")) throw new Error("unreplaced template marker"); + +rmSync(dist, { recursive: true, force: true }); +mkdirSync(path.join(dist, "assets"), { recursive: true }); +mkdirSync(path.join(dist, "dl"), { recursive: true }); +mkdirSync(path.join(dist, "fonts"), { recursive: true }); +writeFileSync(path.join(dist, "index.html"), html); +cpSync(path.join(site, "public"), dist, { recursive: true }); +cpSync(path.join(site, "fonts"), path.join(dist, "fonts"), { recursive: true }); +copyFileSync(path.join(root, ".github", "social-preview.png"), path.join(dist, "social-preview.png")); +copyFileSync(path.join(root, "install.sh"), path.join(dist, "install.sh")); +copyFileSync(path.join(root, "install.ps1"), path.join(dist, "install.ps1")); +copyFileSync(path.join(site, "_headers"), path.join(dist, "_headers")); +copyFileSync(path.join(site, "_redirects"), path.join(dist, "_redirects")); + +await esbuild.build({ + entryPoints: [path.join(site, "src", "main.js")], + bundle: true, + minify: true, + format: "esm", + target: "es2020", + outfile: path.join(dist, "assets", "app.js"), +}); + +const jsGzip = zlib.gzipSync(readFileSync(path.join(dist, "assets", "app.js"))).length; +if (jsGzip > 250 * 1024) throw new Error(`app.js is ${jsGzip} bytes gzipped`); + +const helpers = [ + "hstack-bench", + "hstack-decisions", + "hstack-pr", + "hstack-scan", + "hstack-test", + "hstack-visdiff", +]; +const stage = mkdtempSync(path.join(tmpdir(), "hstack-skills-")); +try { + const top = path.join(stage, "hstack"); + mkdirSync(path.join(top, "skills"), { recursive: true }); + mkdirSync(path.join(top, "scripts"), { recursive: true }); + mkdirSync(path.join(top, "bin"), { recursive: true }); + cpSync(skillsRoot, path.join(top, "skills", "hstack"), { recursive: true }); + cpSync(path.join(root, "scripts", "apply_addons.py"), path.join(top, "scripts", "apply_addons.py")); + for (const helper of helpers) { + cpSync(path.join(root, "bin", helper), path.join(top, "bin", helper)); + } + writeFileSync(path.join(top, "VERSION"), `${version}\n`); + const versioned = path.join(dist, "dl", `hstack-v${version}.tar.gz`); + execFileSync("tar", [ + "--format=ustar", + "--owner=0", + "--group=0", + "--numeric-owner", + "-czf", + versioned, + "-C", + stage, + "hstack", + ], { stdio: "inherit" }); + const members = execFileSync("tar", ["-tzf", versioned], { encoding: "utf8" }).trim().split("\n"); + const tops = new Set(members.map((member) => member.replace(/^\.\//, "").split("/")[0]).filter(Boolean)); + if (tops.size !== 1 || !tops.has("hstack")) { + throw new Error(`archive must have one top-level directory, saw ${[...tops].join(",")}`); + } + for (const member of members) { + const norm = member.replace(/^\.\//, ""); + if (!norm || norm.startsWith("/") || norm.split("/").includes("..")) { + throw new Error(`unsafe archive member ${member}`); + } + } + const required = [ + "hstack/VERSION", + "hstack/skills/hstack/hstack/SKILL.md", + "hstack/scripts/apply_addons.py", + ...helpers.map((helper) => `hstack/bin/${helper}`), + ]; + for (const name of required) { + if (!members.some((member) => member.replace(/^\.\//, "") === name)) { + throw new Error(`archive is missing ${name}`); + } + } + const listing = execFileSync("tar", ["-tvzf", versioned], { encoding: "utf8" }); + for (const line of listing.split("\n")) { + if (line.startsWith("l") || line.startsWith("h")) { + throw new Error(`archive contains a link: ${line}`); + } + } + for (const helper of helpers) { + const line = listing.split("\n").find((entry) => entry.endsWith(`hstack/bin/${helper}`)); + if (!line || !/^.r.x/.test(line)) { + throw new Error(`archive helper is not executable: ${helper}`); + } + } + const latest = path.join(dist, "dl", "hstack.tar.gz"); + copyFileSync(versioned, latest); + const hash = crypto.createHash("sha256").update(readFileSync(versioned)).digest("hex"); + writeFileSync(`${versioned}.sha256`, `${hash} hstack-v${version}.tar.gz\n`); + writeFileSync(`${latest}.sha256`, `${hash} hstack.tar.gz\n`); + for (const published of ["install.sh", "install.ps1"]) { + if (readFileSync(path.join(dist, published), "utf8").includes("github.com")) { + throw new Error(`published ${published} still points at GitHub`); + } + } + console.log(`idle ${idle} planning ${planning} bodies ${bodies} js ${jsGzip} gzip ${hash.slice(0, 12)}`); +} finally { + rmSync(stage, { recursive: true, force: true }); +} diff --git a/site/scripts/check-no-html.mjs b/site/scripts/check-no-html.mjs new file mode 100644 index 0000000..d89967d --- /dev/null +++ b/site/scripts/check-no-html.mjs @@ -0,0 +1,71 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { createPreview } from "./preview.mjs"; + +const dist = path.resolve(import.meta.dirname, "..", "dist"); +const html = readFileSync(path.join(dist, "index.html"), "utf8"); +const server = createPreview(); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const { port } = server.address(); +const base = `http://127.0.0.1:${port}`; + +function fail(message) { + console.error(`check-no-html: ${message}`); + process.exitCode = 1; +} + +const urls = new Set(["/favicon.svg", "/social-preview.png", "/install", "/install/", "/install.sh", "/install.ps1"]); +for (const match of html.matchAll(/(?:src|href)="(\/[^"]+)"/g)) urls.add(match[1]); +const style = html.match(/