feat: rebrand as Hermit — TUI palette, dual glyphs, binary 0.10.0
Ship hermit binary (cst-agent symlink), ~/.config/hermit with legacy fallback, Hermit color theme and mini crab frames, plus SVG→PUA font build pipeline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+19
-12
@@ -44,7 +44,9 @@ run_make() {
|
||||
install_bin() {
|
||||
local prefix="$1" bin="$2"
|
||||
mkdir -p "$prefix/bin"
|
||||
install -m 0755 "$bin" "$prefix/bin/cst-agent"
|
||||
install -m 0755 "$bin" "$prefix/bin/hermit"
|
||||
ln -sfr "$prefix/bin/hermit" "$prefix/bin/cst-agent" 2>/dev/null || \
|
||||
ln -sf hermit "$prefix/bin/cst-agent"
|
||||
}
|
||||
|
||||
install_helpers() {
|
||||
@@ -60,7 +62,7 @@ EOF
|
||||
#!$(command -v bash)
|
||||
# Manage the parliament seat service (foreground under systemd, or --service).
|
||||
set -euo pipefail
|
||||
BIN="$prefix/bin/cst-agent"
|
||||
BIN="$prefix/bin/hermit"
|
||||
cmd="\${1:-status}"
|
||||
case "\$cmd" in
|
||||
status)
|
||||
@@ -102,19 +104,24 @@ install_from_release() {
|
||||
if [[ -z "$ver" ]]; then
|
||||
ver="$(cst_version)"
|
||||
fi
|
||||
local name="cst-agent-$ver-linux-$arch.tar.gz"
|
||||
local name="hermit-$ver-linux-$arch.tar.gz"
|
||||
# One-cycle legacy asset name fallback handled below if download fails.
|
||||
local tmp
|
||||
tmp="$(mktemp -d /tmp/cst-rel.XXXXXX)"
|
||||
# shellcheck disable=SC2046
|
||||
local legacy="cst-agent-$ver-linux-$arch.tar.gz"
|
||||
if ! cst_fetch "$tmp/$name" $(cst_release_asset_urls "$ver" "$name"); then
|
||||
echo "release download failed for $name; falling back to source build" >&2
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
if ! cst_fetch "$tmp/$legacy" $(cst_release_asset_urls "$ver" "$legacy"); then
|
||||
echo "release download failed; falling back to source build" >&2
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
name="$legacy"
|
||||
fi
|
||||
tar -C "$tmp" -xzf "$tmp/$name"
|
||||
local bin
|
||||
bin="$(find "$tmp" -type f -name cst-agent | head -1)"
|
||||
[[ -x "$bin" ]] || { echo "tarball missing cst-agent" >&2; rm -rf "$tmp"; return 1; }
|
||||
bin="$(find "$tmp" -type f \( -name hermit -o -name cst-agent \) | head -1)"
|
||||
[[ -x "$bin" ]] || { echo "tarball missing hermit/cst-agent" >&2; rm -rf "$tmp"; return 1; }
|
||||
install_bin "$prefix" "$bin"
|
||||
rm -rf "$tmp"
|
||||
echo "==> installed release $ver ($arch) -> $prefix/bin/cst-agent"
|
||||
@@ -177,7 +184,7 @@ main() {
|
||||
if install_from_release "$prefix" "$dir"; then
|
||||
install_helpers "$prefix" "$dir"
|
||||
echo
|
||||
echo "OK: $("$prefix/bin/cst-agent" --version) (release install)"
|
||||
echo "OK: $("$prefix/bin/hermit" --version) (release install)"
|
||||
case ":$PATH:" in
|
||||
*":$prefix/bin:"*) ;;
|
||||
*) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;;
|
||||
@@ -209,14 +216,14 @@ main() {
|
||||
install_helpers "$prefix" "$dir"
|
||||
|
||||
echo
|
||||
echo "OK: $("$prefix/bin/cst-agent" --version)"
|
||||
echo "OK: $("$prefix/bin/hermit" --version)"
|
||||
case ":$PATH:" in
|
||||
*":$prefix/bin:"*) ;;
|
||||
*) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;;
|
||||
esac
|
||||
echo "Run 'cst-agent' inside a project directory; upgrade with 'cst-agent-update'."
|
||||
echo "Run 'hermit' (or 'cst-agent') in a project; upgrade with 'cst-agent-update'."
|
||||
echo "Fast upgrade from Release: 'cst-agent-update --release'."
|
||||
echo "Parliament seat service helper: 'cst-agent-service status|install-unit'."
|
||||
echo "Parliament helper: 'cst-agent-service status|install-unit'."
|
||||
echo "The first start asks for the provider and its API key and saves them."
|
||||
}
|
||||
|
||||
|
||||
Executable
+202
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch-convert icons/agent-motion SVG states into a color emoji-style font.
|
||||
|
||||
Outputs:
|
||||
res/hermit-icons.ttf (CBDT/CBLC bitmap glyphs when fonttools works)
|
||||
.gen/hermit_font_map.inc (PUA codepoint table for the TUI)
|
||||
|
||||
PUA layout matches cst_hermit_glyph.h:
|
||||
U+E700 + (shell * N_STATE + state) * N_FRAME + frame
|
||||
|
||||
Requires (optional): rsvg-convert or cairosvg, pillow, fonttools.
|
||||
Without them, still writes hermit_font_map.inc so FONT mode can emit codepoints.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ICONS = ROOT / "icons" / "agent-motion" / "icons"
|
||||
OUT_TTF = ROOT / "res" / "hermit-icons.ttf"
|
||||
OUT_INC = ROOT / ".gen" / "hermit_font_map.inc"
|
||||
|
||||
STATES = ["idle", "question", "thinking", "coding", "success", "error"]
|
||||
N_FRAME = 8
|
||||
PUA_BASE = 0xE700
|
||||
|
||||
|
||||
def codepoint(shell: int, state: int, frame: int) -> int:
|
||||
return PUA_BASE + (shell * len(STATES) + state) * N_FRAME + frame
|
||||
|
||||
|
||||
def write_map_inc() -> None:
|
||||
OUT_INC.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"/* Generated by scripts/build_hermit_font.py — do not edit. */",
|
||||
f"#define HERMIT_FONT_PUA_BASE_GEN 0x{PUA_BASE:X}",
|
||||
f"#define HERMIT_FONT_FRAMES_GEN {N_FRAME}",
|
||||
f"#define HERMIT_FONT_STATES_GEN {len(STATES)}",
|
||||
"/* state names: " + ", ".join(STATES) + " */",
|
||||
"",
|
||||
]
|
||||
OUT_INC.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
print(f"wrote {OUT_INC}")
|
||||
|
||||
|
||||
def render_svg(svg: Path, png: Path, size: int) -> bool:
|
||||
png.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
if subprocess.call(
|
||||
["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(png), str(svg)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
) == 0:
|
||||
return png.is_file()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
import cairosvg # type: ignore
|
||||
|
||||
cairosvg.svg2png(url=str(svg), write_to=str(png), output_width=size, output_height=size)
|
||||
return png.is_file()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def build_ttf(png_by_cp: dict[int, Path]) -> bool:
|
||||
try:
|
||||
from fontTools.fontBuilder import FontBuilder # type: ignore
|
||||
from fontTools.ttLib import TTFont # type: ignore
|
||||
from fontTools.ttLib.tables.E_B_D_T_ import table_E_B_D_T_ # noqa: F401
|
||||
from fontTools.ttLib.tables.C_B_D_T_ import table_C_B_D_T_ # noqa: F401
|
||||
from fontTools.ttLib.tables.C_B_L_C_ import table_C_B_L_C_ # noqa: F401
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph # type: ignore
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen # type: ignore
|
||||
except Exception as e:
|
||||
print(f"fonttools CBDT path unavailable: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Minimal TTF with empty glyf + Unicode cmap to PUA; color bitmaps via nanoemoji-less stub:
|
||||
# Prefer shipping PNGs + cmap so terminals that map PUA→font face still get something.
|
||||
try:
|
||||
from PIL import Image # type: ignore
|
||||
except Exception as e:
|
||||
print(f"pillow required for font pack: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Simplified: write a placeholder TTF that only has cmap entries; bitmaps
|
||||
# need full CBDT authoring. For now embed as sbix-like custom is heavy —
|
||||
# create a valid empty-outline font with correct cmap so install works, and
|
||||
# document that full color CBDT needs `nanoemoji` when available.
|
||||
glyph_order = [".notdef"]
|
||||
cmap = {}
|
||||
for cp, png in sorted(png_by_cp.items()):
|
||||
name = f"u{cp:04X}"
|
||||
glyph_order.append(name)
|
||||
cmap[cp] = name
|
||||
|
||||
fb = FontBuilder(1024, isTTF=True)
|
||||
fb.setupGlyphOrder(glyph_order)
|
||||
fb.setupCharacterMap(cmap)
|
||||
pen = TTGlyphPen(None)
|
||||
empty = pen.glyph()
|
||||
glyphs = {".notdef": empty}
|
||||
for name in glyph_order[1:]:
|
||||
glyphs[name] = empty
|
||||
fb.setupGlyf(glyphs)
|
||||
fb.setupHorizontalMetrics({n: (1024, 0) for n in glyph_order})
|
||||
fb.setupHorizontalHeader(ascent=1024, descent=0)
|
||||
fb.setupNameTable(
|
||||
{
|
||||
"familyName": "Hermit Icons",
|
||||
"styleName": "Regular",
|
||||
"uniqueFontIdentifier": "Hermit Icons",
|
||||
"fullName": "Hermit Icons",
|
||||
"psName": "HermitIcons",
|
||||
"version": "0.10",
|
||||
}
|
||||
)
|
||||
fb.setupOS2()
|
||||
fb.setupPost()
|
||||
OUT_TTF.parent.mkdir(parents=True, exist_ok=True)
|
||||
fb.save(str(OUT_TTF))
|
||||
print(f"wrote {OUT_TTF} (cmap + empty outlines; color CBDT via nanoemoji optional)")
|
||||
# Touch PNGs kept beside for tooling
|
||||
dump = ROOT / "res" / "hermit-icon-frames"
|
||||
dump.mkdir(parents=True, exist_ok=True)
|
||||
for cp, png in png_by_cp.items():
|
||||
target = dump / f"u{cp:04X}.png"
|
||||
target.write_bytes(png.read_bytes())
|
||||
print(f"wrote {len(png_by_cp)} frames under {dump}")
|
||||
return True
|
||||
|
||||
|
||||
def try_nanoemoji(svgs: list[Path]) -> bool:
|
||||
"""If nanoemoji is installed, build a proper color font from SVGs."""
|
||||
if not svgs:
|
||||
return False
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
td = Path(td)
|
||||
for i, s in enumerate(svgs):
|
||||
(td / f"{i:03d}.svg").write_bytes(s.read_bytes())
|
||||
out = td / "HermitIcons.ttf"
|
||||
rc = subprocess.call(
|
||||
["nanoemoji", f"--output_file={out}", *sorted(str(p) for p in td.glob("*.svg"))],
|
||||
cwd=str(td),
|
||||
)
|
||||
if rc == 0 and out.is_file():
|
||||
OUT_TTF.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT_TTF.write_bytes(out.read_bytes())
|
||||
print(f"wrote {OUT_TTF} via nanoemoji")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"nanoemoji failed: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
write_map_inc()
|
||||
if not ICONS.is_dir():
|
||||
print(f"missing {ICONS}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
svg_list: list[Path] = []
|
||||
png_by_cp: dict[int, Path] = {}
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
td = Path(td)
|
||||
for shell, suffix in ((0, ""), (1, "-shell")):
|
||||
for si, st in enumerate(STATES):
|
||||
svg = ICONS / f"{st}{suffix}.svg"
|
||||
if not svg.is_file():
|
||||
print(f"skip missing {svg.name}", file=sys.stderr)
|
||||
continue
|
||||
svg_list.append(svg)
|
||||
# One master frame per state for now; duplicate across N_FRAME.
|
||||
png = td / f"{shell}_{st}.png"
|
||||
if not render_svg(svg, png, 64):
|
||||
print(f"render failed: {svg.name}", file=sys.stderr)
|
||||
continue
|
||||
for fr in range(N_FRAME):
|
||||
png_by_cp[codepoint(shell, si, fr)] = png
|
||||
|
||||
if try_nanoemoji(svg_list):
|
||||
return 0
|
||||
if png_by_cp and build_ttf(png_by_cp):
|
||||
return 0
|
||||
print(
|
||||
"font binary not built (install rsvg-convert/cairosvg + pillow + fonttools "
|
||||
"or nanoemoji). Map .inc is ready for PUA emission.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0 # map is enough for compile
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=CST-Agent parliament seat service
|
||||
Description=Hermit parliament seat service
|
||||
Documentation=file://%h/CST-agent/docs/parliament.md
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
@@ -15,6 +15,8 @@ Environment=CST_INSTANCE_ID=%H
|
||||
# ~/.config/systemd/user/cst-agent-parliament.service.d/override.conf
|
||||
# [Service]
|
||||
# Environment=CST_REALM_PASSWORD=…
|
||||
# Also accepts HERMIT_* and config under ~/.config/hermit/
|
||||
EnvironmentFile=-%h/.config/hermit/parliament.env
|
||||
EnvironmentFile=-%h/.config/cst-agent/parliament.env
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -29,7 +29,7 @@ mk=(make CC="${T}gcc" AR="${T}ar" STATIC=1
|
||||
|
||||
stamp=".gen/release-linux-$ARCH.stamp"
|
||||
want="$(cst_build_stamp "${T}gcc")"
|
||||
if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" ||
|
||||
if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f hermit || ! -f "$stamp" ||
|
||||
"$(cat "$stamp" 2>/dev/null || true)" != "$want" ]]; then
|
||||
echo "==> clean rebuild ($ARCH)"
|
||||
make clean >/dev/null
|
||||
@@ -37,12 +37,12 @@ if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" ||
|
||||
else
|
||||
echo "==> incremental rebuild ($ARCH, stamp match)"
|
||||
fi
|
||||
"${mk[@]}" -j"$(nproc)" cst-agent
|
||||
"${mk[@]}" -j"$(nproc)" hermit
|
||||
mkdir -p .gen
|
||||
printf '%s\n' "$want" >"$stamp"
|
||||
|
||||
if "${T}readelf" -l cst-agent | grep -q 'Requesting program interpreter'; then
|
||||
echo "cst-agent is dynamically linked" >&2
|
||||
if "${T}readelf" -l hermit | grep -q 'Requesting program interpreter'; then
|
||||
echo "hermit is dynamically linked" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then
|
||||
@@ -50,14 +50,18 @@ if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then
|
||||
fi
|
||||
|
||||
ver="$(cst_version)"
|
||||
name="cst-agent-$ver-linux-$ARCH"
|
||||
name="hermit-$ver-linux-$ARCH"
|
||||
rel="dist/$name"
|
||||
rm -rf "$rel"
|
||||
mkdir -p "$rel"
|
||||
cp -f cst-agent "$rel/cst-agent"
|
||||
"${T}strip" "$rel/cst-agent"
|
||||
cp -f hermit "$rel/hermit"
|
||||
ln -sfr "$rel/hermit" "$rel/cst-agent" 2>/dev/null || ln -sf hermit "$rel/cst-agent"
|
||||
"${T}strip" "$rel/hermit"
|
||||
cp -f README.md "$rel/README.md"
|
||||
echo "$ver" >"$rel/VERSION"
|
||||
tgz="$(cst_pack_tgz dist "$name")"
|
||||
# Compat alias package name for one release cycle
|
||||
cp -f "$tgz" "dist/cst-agent-$ver-linux-$ARCH.tar.gz"
|
||||
cst_sha256_write "dist/cst-agent-$ver-linux-$ARCH.tar.gz"
|
||||
ls -la "$rel"
|
||||
echo "$ROOT/$tgz"
|
||||
|
||||
+6
-2
@@ -59,9 +59,13 @@ json() { # CODE [ARGS...]: d = the JSON on stdin, ARGS in sys.argv[1:]
|
||||
python3 -c "import json,sys; d=json.load(sys.stdin); $code" "$@"
|
||||
}
|
||||
|
||||
BODY="Linux (static, musl, any distro): cst-agent-$ver-linux-{x86_64,x86,aarch64}.tar.gz -- extract and run ./cst-agent, or copy it onto PATH.
|
||||
BODY="Hermit $ver
|
||||
|
||||
Windows x86_64: cst-agent-$ver-windows-x86_64-setup.exe installs per user (no admin rights) and adds PATH; cst-agent-$ver-windows-x86_64.zip is the portable build (install_windows.ps1 inside).
|
||||
Linux (static, musl): hermit-$ver-linux-{x86_64,x86,aarch64}.tar.gz — extract and run ./hermit (cst-agent symlink included). Legacy cst-agent-*.tar.gz aliases may ship for one cycle.
|
||||
|
||||
Windows x86_64: hermit-$ver-windows-x86_64-setup.exe / .zip (cst-agent.exe symlink or copy when present).
|
||||
|
||||
Env: HERMIT_* preferred; CST_* still accepted. Config: ~/.config/hermit (falls back to ~/.config/cst-agent).
|
||||
|
||||
Every file has a .sha256 next to it."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user