chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
Executable
+297
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Gortex Comprehensive Benchmark Suite for Raspberry Pi / Low-Resource Devices
|
||||
# =============================================================================
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/bench-rpi.sh # Full suite
|
||||
# ./scripts/bench-rpi.sh --quick # Quick smoke test (1 iteration)
|
||||
# ./scripts/bench-rpi.sh --profile # With CPU/memory profiles
|
||||
# ./scripts/bench-rpi.sh --compare <file> # Compare against baseline
|
||||
# ./scripts/bench-rpi.sh --package graph # Single package
|
||||
#
|
||||
# Output:
|
||||
# results/bench-<timestamp>.txt # Raw benchmark output
|
||||
# results/bench-<timestamp>.json # System info + summary
|
||||
# results/profiles/ # CPU/mem profiles (if --profile)
|
||||
#
|
||||
# Requirements:
|
||||
# - Go 1.26+. Pure-Go tree-sitter runtime — no C toolchain needed.
|
||||
# - benchstat (go install golang.org/x/perf/cmd/benchstat@latest)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RESULTS_DIR="results"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BENCH_FILE="${RESULTS_DIR}/bench-${TIMESTAMP}.txt"
|
||||
INFO_FILE="${RESULTS_DIR}/bench-${TIMESTAMP}.json"
|
||||
PROFILE_DIR="${RESULTS_DIR}/profiles/${TIMESTAMP}"
|
||||
|
||||
BENCH_COUNT=3
|
||||
BENCH_TIME="2s"
|
||||
BENCH_TIMEOUT="30m"
|
||||
DO_PROFILE=false
|
||||
COMPARE_FILE=""
|
||||
SINGLE_PACKAGE=""
|
||||
|
||||
# Packages to benchmark (in dependency order).
|
||||
BENCH_PACKAGES=(
|
||||
"./internal/graph/"
|
||||
"./internal/search/"
|
||||
"./internal/parser/languages/"
|
||||
"./internal/resolver/"
|
||||
"./internal/indexer/"
|
||||
"./internal/query/"
|
||||
"./internal/analysis/"
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse arguments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--quick)
|
||||
BENCH_COUNT=1
|
||||
BENCH_TIME="500ms"
|
||||
BENCH_TIMEOUT="10m"
|
||||
shift
|
||||
;;
|
||||
--profile)
|
||||
DO_PROFILE=true
|
||||
shift
|
||||
;;
|
||||
--compare)
|
||||
COMPARE_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--package)
|
||||
SINGLE_PACKAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--count)
|
||||
BENCH_COUNT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--benchtime)
|
||||
BENCH_TIME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
head -20 "$0" | tail -15
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mkdir -p "${RESULTS_DIR}"
|
||||
if $DO_PROFILE; then
|
||||
mkdir -p "${PROFILE_DIR}"
|
||||
fi
|
||||
|
||||
# Filter to single package if requested.
|
||||
if [[ -n "$SINGLE_PACKAGE" ]]; then
|
||||
BENCH_PACKAGES=("./internal/${SINGLE_PACKAGE}/")
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System information
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "=== Gortex Benchmark Suite ==="
|
||||
echo "Timestamp: ${TIMESTAMP}"
|
||||
echo ""
|
||||
|
||||
collect_sysinfo() {
|
||||
local arch os cpus mem_total_kb mem_total_mb go_version kernel
|
||||
arch=$(uname -m)
|
||||
os=$(uname -s)
|
||||
kernel=$(uname -r)
|
||||
go_version=$(go version 2>/dev/null | awk '{print $3}')
|
||||
|
||||
# CPU count
|
||||
if [[ "$os" == "Darwin" ]]; then
|
||||
cpus=$(sysctl -n hw.ncpu 2>/dev/null || echo "unknown")
|
||||
mem_total_mb=$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 / 1024 ))
|
||||
elif [[ "$os" == "Linux" ]]; then
|
||||
cpus=$(nproc 2>/dev/null || echo "unknown")
|
||||
mem_total_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
|
||||
mem_total_mb=$(( mem_total_kb / 1024 ))
|
||||
else
|
||||
cpus="unknown"
|
||||
mem_total_mb=0
|
||||
fi
|
||||
|
||||
# Detect RPi
|
||||
local is_rpi=false
|
||||
if [[ -f /proc/device-tree/model ]]; then
|
||||
if grep -qi "raspberry" /proc/device-tree/model 2>/dev/null; then
|
||||
is_rpi=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# CPU model
|
||||
local cpu_model="unknown"
|
||||
if [[ "$os" == "Darwin" ]]; then
|
||||
cpu_model=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "unknown")
|
||||
elif [[ "$os" == "Linux" ]]; then
|
||||
cpu_model=$(awk -F: '/model name/ {print $2; exit}' /proc/cpuinfo 2>/dev/null | xargs || echo "unknown")
|
||||
fi
|
||||
|
||||
# CPU frequency (MHz)
|
||||
local cpu_freq="unknown"
|
||||
if [[ "$os" == "Linux" ]] && [[ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq ]]; then
|
||||
cpu_freq=$(( $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq) / 1000 ))
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"timestamp": "${TIMESTAMP}",
|
||||
"arch": "${arch}",
|
||||
"os": "${os}",
|
||||
"kernel": "${kernel}",
|
||||
"go_version": "${go_version}",
|
||||
"cpu_model": "${cpu_model}",
|
||||
"cpu_cores": ${cpus},
|
||||
"cpu_freq_mhz": "${cpu_freq}",
|
||||
"memory_mb": ${mem_total_mb},
|
||||
"is_raspberry_pi": ${is_rpi},
|
||||
"gomaxprocs": $(go env GOMAXPROCS 2>/dev/null || echo 0),
|
||||
"cgo_enabled": "$(go env CGO_ENABLED 2>/dev/null || echo unknown)",
|
||||
"goarch": "$(go env GOARCH 2>/dev/null || echo unknown)",
|
||||
"goos": "$(go env GOOS 2>/dev/null || echo unknown)",
|
||||
"bench_count": ${BENCH_COUNT},
|
||||
"bench_time": "${BENCH_TIME}"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
SYSINFO=$(collect_sysinfo)
|
||||
echo "$SYSINFO" > "${INFO_FILE}"
|
||||
echo "System: $(echo "$SYSINFO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{d[\"cpu_model\"]} | {d[\"cpu_cores\"]} cores | {d[\"memory_mb\"]}MB RAM | {d[\"arch\"]} | Go {d[\"go_version\"]}')" 2>/dev/null || echo "System info saved to ${INFO_FILE}")"
|
||||
echo "Output: ${BENCH_FILE}"
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Run benchmarks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "Running benchmarks (count=${BENCH_COUNT}, benchtime=${BENCH_TIME})..."
|
||||
echo ""
|
||||
|
||||
# Ensure binary builds.
|
||||
echo "--- Building gortex ---"
|
||||
go build -o /dev/null ./cmd/gortex/ 2>&1 || {
|
||||
echo "ERROR: Build failed."
|
||||
exit 1
|
||||
}
|
||||
echo "Build OK"
|
||||
echo ""
|
||||
|
||||
run_bench() {
|
||||
local pkg="$1"
|
||||
local pkg_name
|
||||
pkg_name=$(basename "$pkg" | tr -d '/')
|
||||
|
||||
echo "--- Benchmarking: ${pkg_name} ---"
|
||||
|
||||
local extra_flags="-bench=. -benchmem -count=${BENCH_COUNT} -benchtime=${BENCH_TIME} -timeout=${BENCH_TIMEOUT}"
|
||||
|
||||
if $DO_PROFILE; then
|
||||
local cpu_prof="${PROFILE_DIR}/${pkg_name}_cpu.prof"
|
||||
local mem_prof="${PROFILE_DIR}/${pkg_name}_mem.prof"
|
||||
extra_flags="${extra_flags} -cpuprofile=${cpu_prof} -memprofile=${mem_prof}"
|
||||
fi
|
||||
|
||||
# Run with -race disabled for accurate timing (race detector adds ~10x overhead).
|
||||
if ! go test ${extra_flags} -run='^$' "${pkg}" 2>&1 | tee -a "${BENCH_FILE}"; then
|
||||
echo "WARNING: Benchmarks failed for ${pkg_name}" >&2
|
||||
fi
|
||||
echo "" >> "${BENCH_FILE}"
|
||||
}
|
||||
|
||||
# Header in results file.
|
||||
{
|
||||
echo "# Gortex Benchmark Results"
|
||||
echo "# Timestamp: ${TIMESTAMP}"
|
||||
echo "# System: $(uname -m) / $(uname -s) / $(go version)"
|
||||
echo "# Count: ${BENCH_COUNT}, BenchTime: ${BENCH_TIME}"
|
||||
echo ""
|
||||
} > "${BENCH_FILE}"
|
||||
|
||||
for pkg in "${BENCH_PACKAGES[@]}"; do
|
||||
run_bench "$pkg"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Benchmark Complete ==="
|
||||
echo "Results: ${BENCH_FILE}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary statistics
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if command -v benchstat &>/dev/null; then
|
||||
echo ""
|
||||
echo "--- Summary (benchstat) ---"
|
||||
benchstat "${BENCH_FILE}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compare against baseline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [[ -n "$COMPARE_FILE" ]]; then
|
||||
if command -v benchstat &>/dev/null; then
|
||||
echo ""
|
||||
echo "--- Comparison vs Baseline ---"
|
||||
benchstat "$COMPARE_FILE" "${BENCH_FILE}"
|
||||
else
|
||||
echo "Install benchstat for comparison: go install golang.org/x/perf/cmd/benchstat@latest"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RPi-specific warnings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MEM_MB=$(echo "$SYSINFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['memory_mb'])" 2>/dev/null || echo 0)
|
||||
if [[ "$MEM_MB" -gt 0 ]] && [[ "$MEM_MB" -lt 2048 ]]; then
|
||||
echo ""
|
||||
echo "⚠️ Low memory detected (${MEM_MB}MB). Consider:"
|
||||
echo " - Setting GOGC=50 to reduce GC pressure"
|
||||
echo " - Using --quick for faster iteration"
|
||||
echo " - Running individual packages with --package <name>"
|
||||
fi
|
||||
|
||||
IS_RPI=$(echo "$SYSINFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['is_raspberry_pi'])" 2>/dev/null || echo false)
|
||||
if [[ "$IS_RPI" == "true" ]]; then
|
||||
echo ""
|
||||
echo "🍓 Raspberry Pi detected! Tips:"
|
||||
echo " - Ensure adequate cooling (benchmarks are CPU-intensive)"
|
||||
echo " - Consider running with: GOMAXPROCS=2 ./scripts/bench-rpi.sh"
|
||||
echo " - Use --quick for initial validation"
|
||||
fi
|
||||
|
||||
if $DO_PROFILE; then
|
||||
echo ""
|
||||
echo "--- Profiles ---"
|
||||
echo "CPU profiles: ${PROFILE_DIR}/*_cpu.prof"
|
||||
echo "Mem profiles: ${PROFILE_DIR}/*_mem.prof"
|
||||
echo ""
|
||||
echo "Analyze with:"
|
||||
echo " go tool pprof -http=:8080 ${PROFILE_DIR}/<pkg>_cpu.prof"
|
||||
echo " go tool pprof -top ${PROFILE_DIR}/<pkg>_mem.prof"
|
||||
fi
|
||||
@@ -0,0 +1,182 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Gortex one-line installer for Windows (PowerShell).
|
||||
|
||||
.DESCRIPTION
|
||||
Downloads the signed Windows release archive, verifies its SHA-256
|
||||
checksum, installs the self-contained gortex.exe, and puts the install
|
||||
directory on the user PATH.
|
||||
|
||||
Usage:
|
||||
irm https://get.gortex.dev/install.ps1 | iex
|
||||
|
||||
Or, from a checkout:
|
||||
powershell -ExecutionPolicy Bypass -File scripts/install.ps1
|
||||
|
||||
Configuration via environment variables (all optional):
|
||||
GORTEX_VERSION Release tag to install ("latest" or "v0.15.0")
|
||||
GORTEX_INSTALL_DIR Install directory (default: %LOCALAPPDATA%\Programs\gortex)
|
||||
GORTEX_NO_VERIFY Set to skip the SHA-256 checksum verification
|
||||
GORTEX_NO_PATH Set to skip the user PATH update
|
||||
GORTEX_FORCE Set to overwrite an existing binary without backup
|
||||
GORTEX_DOWNLOAD_BASE Override the release download base URL (for testing)
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$Repo = 'zzet/gortex'
|
||||
$BinName = 'gortex.exe'
|
||||
$DownloadBase = if ($env:GORTEX_DOWNLOAD_BASE) { $env:GORTEX_DOWNLOAD_BASE } `
|
||||
else { "https://github.com/$Repo/releases" }
|
||||
|
||||
function Write-Info($msg) { Write-Host "==> $msg" -ForegroundColor Blue }
|
||||
function Write-Ok($msg) { Write-Host " ok $msg" -ForegroundColor Green }
|
||||
function Write-Warn($msg) { Write-Host " ! $msg" -ForegroundColor Yellow }
|
||||
function Die($msg) { Write-Host " x $msg" -ForegroundColor Red; exit 1 }
|
||||
|
||||
function Get-Arch {
|
||||
# We publish a single windows/amd64 archive. Windows on ARM runs x64
|
||||
# binaries transparently under emulation, so amd64 is the right asset
|
||||
# everywhere except genuine 32-bit hosts.
|
||||
switch ($env:PROCESSOR_ARCHITECTURE) {
|
||||
'AMD64' { return 'amd64' }
|
||||
'ARM64' { return 'amd64' }
|
||||
'x86' { Die 'unsupported architecture: x86 (64-bit Windows required)' }
|
||||
default { Die "unsupported architecture: $($env:PROCESSOR_ARCHITECTURE)" }
|
||||
}
|
||||
}
|
||||
|
||||
function Add-ToUserPath($dir) {
|
||||
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$parts = @()
|
||||
if ($current) { $parts = $current -split ';' | Where-Object { $_ -ne '' } }
|
||||
if ($parts -contains $dir) {
|
||||
Write-Ok "$dir already on the user PATH"
|
||||
return
|
||||
}
|
||||
$updated = (($parts + $dir) -join ';')
|
||||
[Environment]::SetEnvironmentVariable('Path', $updated, 'User')
|
||||
# Refresh the current session so the version banner below resolves.
|
||||
$env:Path = "$env:Path;$dir"
|
||||
Write-Ok "added $dir to the user PATH (open a new shell to pick it up)"
|
||||
}
|
||||
|
||||
function Main {
|
||||
$arch = Get-Arch
|
||||
$version = if ($env:GORTEX_VERSION) { $env:GORTEX_VERSION } else { 'latest' }
|
||||
$installDir = if ($env:GORTEX_INSTALL_DIR) { $env:GORTEX_INSTALL_DIR } `
|
||||
else { Join-Path $env:LOCALAPPDATA 'Programs\gortex' }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Gortex installer' -ForegroundColor White
|
||||
Write-Host " os: windows"
|
||||
Write-Host " arch: $arch"
|
||||
Write-Host " version: $version"
|
||||
Write-Host " target: $installDir\$BinName"
|
||||
Write-Host ''
|
||||
|
||||
$asset = "gortex_windows_${arch}.zip"
|
||||
if ($version -eq 'latest') {
|
||||
$baseUrl = "$DownloadBase/latest/download"
|
||||
} else {
|
||||
$tag = if ($version.StartsWith('v')) { $version } else { "v$version" }
|
||||
$baseUrl = "$DownloadBase/download/$tag"
|
||||
}
|
||||
$assetUrl = "$baseUrl/$asset"
|
||||
$checksumsUrl = "$baseUrl/checksums.txt"
|
||||
|
||||
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("gortex-install-" + [System.Guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
||||
try {
|
||||
$zipPath = Join-Path $tmp $asset
|
||||
Write-Info "downloading $asset"
|
||||
try {
|
||||
Invoke-WebRequest -Uri $assetUrl -OutFile $zipPath -UseBasicParsing
|
||||
} catch {
|
||||
Die "download failed: $assetUrl`n $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if (-not $env:GORTEX_NO_VERIFY) {
|
||||
Write-Info 'downloading checksums.txt'
|
||||
$checksumsPath = Join-Path $tmp 'checksums.txt'
|
||||
try {
|
||||
Invoke-WebRequest -Uri $checksumsUrl -OutFile $checksumsPath -UseBasicParsing
|
||||
# checksums.txt is `<sha256> <filename>` per goreleaser default.
|
||||
$expected = $null
|
||||
foreach ($line in Get-Content $checksumsPath) {
|
||||
$cols = $line -split '\s+'
|
||||
if ($cols.Count -ge 2 -and ($cols[1] -eq $asset -or $cols[1] -eq "*$asset")) {
|
||||
$expected = $cols[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $expected) {
|
||||
Write-Warn "checksums.txt did not contain $asset; skipping verification"
|
||||
} else {
|
||||
$actual = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower()
|
||||
if ($actual -ne $expected.ToLower()) {
|
||||
Die "checksum mismatch on $asset`n expected: $expected`n actual: $actual"
|
||||
}
|
||||
Write-Ok "sha256 verified ($asset)"
|
||||
}
|
||||
} catch {
|
||||
Write-Warn 'could not fetch checksums.txt; skipping verification'
|
||||
}
|
||||
} else {
|
||||
Write-Warn 'verification disabled (GORTEX_NO_VERIFY)'
|
||||
}
|
||||
|
||||
Write-Info 'extracting'
|
||||
$staging = Join-Path $tmp 'extract'
|
||||
Expand-Archive -Path $zipPath -DestinationPath $staging -Force
|
||||
$extracted = Join-Path $staging $BinName
|
||||
if (-not (Test-Path $extracted)) {
|
||||
Die "archive did not contain a $BinName binary"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||
$target = Join-Path $installDir $BinName
|
||||
if ((Test-Path $target) -and (-not $env:GORTEX_FORCE)) {
|
||||
$backup = "$target.previous"
|
||||
Write-Info "backing up existing binary to $backup"
|
||||
Move-Item -Path $target -Destination $backup -Force
|
||||
}
|
||||
# gortex.exe is a single self-contained binary — the mingw C/C++
|
||||
# runtime is statically linked into it — so install is a one-file
|
||||
# copy with nothing else to place beside it.
|
||||
Copy-Item -Path $extracted -Destination $target -Force
|
||||
Write-Ok "installed $target"
|
||||
|
||||
if (-not $env:GORTEX_NO_PATH) {
|
||||
Add-ToUserPath $installDir
|
||||
}
|
||||
|
||||
# If a daemon is already running an older binary, restart it onto
|
||||
# the new one. Best-effort — never block the install on this.
|
||||
& $target daemon status *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Info 'restarting running daemon onto new binary'
|
||||
& $target daemon restart *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warn "daemon restart failed; run 'gortex daemon restart' manually"
|
||||
}
|
||||
}
|
||||
|
||||
$versionOut = (& $target version) 2>$null
|
||||
if ($versionOut) { Write-Ok "$versionOut" }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Next steps:' -ForegroundColor White
|
||||
Write-Host " - gortex install one-time machine setup (MCP, skills, slash commands)"
|
||||
Write-Host " - gortex init run inside a repo to wire up your AI assistant"
|
||||
Write-Host ''
|
||||
Write-Host "Docs: https://github.com/$Repo"
|
||||
Write-Host ''
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Main
|
||||
Executable
+343
@@ -0,0 +1,343 @@
|
||||
#!/bin/sh
|
||||
# Gortex one-line installer for macOS and Linux.
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://get.gortex.dev | sh
|
||||
# curl -fsSL https://raw.githubusercontent.com/zzet/gortex/main/scripts/install.sh | sh
|
||||
#
|
||||
# Configuration via environment variables (all optional):
|
||||
# GORTEX_VERSION Release tag to install ("latest" or "v0.15.0")
|
||||
# GORTEX_INSTALL_DIR Install directory (default: $HOME/.local/bin)
|
||||
# GORTEX_NO_VERIFY Set to skip checksum + cosign verification
|
||||
# GORTEX_NO_PATH Set to skip PATH update in shell rc
|
||||
# GORTEX_NO_BREW Set to skip Homebrew (macOS) and force the tarball path
|
||||
# GORTEX_FORCE Set to overwrite an existing binary without backup
|
||||
# GORTEX_DOWNLOAD_BASE Override release download base URL (for testing)
|
||||
|
||||
set -eu
|
||||
|
||||
GORTEX_REPO="zzet/gortex"
|
||||
GORTEX_BIN="gortex"
|
||||
DOWNLOAD_BASE="${GORTEX_DOWNLOAD_BASE:-https://github.com/${GORTEX_REPO}/releases}"
|
||||
|
||||
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
|
||||
C_BOLD="$(tput bold)"
|
||||
C_DIM="$(tput dim 2>/dev/null || printf '')"
|
||||
C_RED="$(tput setaf 1)"
|
||||
C_GREEN="$(tput setaf 2)"
|
||||
C_YELLOW="$(tput setaf 3)"
|
||||
C_BLUE="$(tput setaf 4)"
|
||||
C_RESET="$(tput sgr0)"
|
||||
else
|
||||
C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_RESET=""
|
||||
fi
|
||||
|
||||
info() { printf '%s==>%s %s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}" "$1"; }
|
||||
ok() { printf '%s ok%s %s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$1"; }
|
||||
warn() { printf '%s !%s %s\n' "${C_YELLOW}${C_BOLD}" "${C_RESET}" "$1" >&2; }
|
||||
die() { printf '%s x%s %s\n' "${C_RED}${C_BOLD}" "${C_RESET}" "$1" >&2; exit 1; }
|
||||
|
||||
# Pick a downloader once.
|
||||
DOWNLOADER=""
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
DOWNLOADER="curl"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
DOWNLOADER="wget"
|
||||
else
|
||||
die "neither curl nor wget is installed; install one and retry"
|
||||
fi
|
||||
|
||||
http_get() {
|
||||
# $1=url, $2=dest. Returns non-zero on failure (callers decide whether fatal).
|
||||
case "$DOWNLOADER" in
|
||||
curl) curl --fail --silent --show-error --location --output "$2" "$1" ;;
|
||||
wget) wget --quiet --output-document="$2" "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_os() {
|
||||
uname_s="$(uname -s)"
|
||||
case "$uname_s" in
|
||||
Linux) echo linux ;;
|
||||
Darwin) echo darwin ;;
|
||||
# Native Windows shells (Git Bash / MSYS / Cygwin) can't run this
|
||||
# POSIX installer — point users at the PowerShell installer. The
|
||||
# Linux build still runs fine under WSL.
|
||||
MINGW*|MSYS*|CYGWIN*) die "this POSIX installer can't run on native Windows.
|
||||
In PowerShell, run: irm https://get.gortex.dev/install.ps1 | iex
|
||||
(or 'scoop install gortex', or run this script from inside WSL)" ;;
|
||||
*) die "unsupported OS: $uname_s (Linux and macOS supported)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
uname_m="$(uname -m)"
|
||||
case "$uname_m" in
|
||||
x86_64|amd64) echo amd64 ;;
|
||||
aarch64|arm64) echo arm64 ;;
|
||||
*) die "unsupported architecture: $uname_m (amd64 and arm64 supported)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
verify_sha256() {
|
||||
# $1=file, $2=expected_hex
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual="$(sha256sum "$1" | awk '{print $1}')"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
actual="$(shasum -a 256 "$1" | awk '{print $1}')"
|
||||
else
|
||||
warn "no sha256sum or shasum available; cannot verify checksum"
|
||||
return 0
|
||||
fi
|
||||
if [ "$actual" != "$2" ]; then
|
||||
die "checksum mismatch on $(basename "$1")
|
||||
expected: $2
|
||||
actual: $actual"
|
||||
fi
|
||||
ok "sha256 verified ($(basename "$1"))"
|
||||
}
|
||||
|
||||
# Append a PATH export to the user's shell rc file if it isn't already there.
|
||||
# Uses a marker block so re-runs are idempotent.
|
||||
update_path_in_rc() {
|
||||
install_dir="$1"
|
||||
marker_begin="# >>> gortex installer >>>"
|
||||
marker_end="# <<< gortex installer <<<"
|
||||
|
||||
case ":${PATH}:" in
|
||||
*":${install_dir}:"*)
|
||||
ok "$install_dir already on PATH"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
user_shell="$(basename "${SHELL:-sh}")"
|
||||
rcfiles=""
|
||||
case "$user_shell" in
|
||||
bash)
|
||||
[ -f "$HOME/.bashrc" ] && rcfiles="$HOME/.bashrc"
|
||||
# macOS terminals start a login bash, which reads .bash_profile.
|
||||
if [ "$(uname -s)" = Darwin ] && [ -f "$HOME/.bash_profile" ]; then
|
||||
rcfiles="${rcfiles:+$rcfiles }$HOME/.bash_profile"
|
||||
fi
|
||||
[ -z "$rcfiles" ] && rcfiles="$HOME/.bashrc"
|
||||
;;
|
||||
zsh)
|
||||
rcfiles="${ZDOTDIR:-$HOME}/.zshrc"
|
||||
;;
|
||||
fish)
|
||||
rcfiles="$HOME/.config/fish/config.fish"
|
||||
;;
|
||||
*)
|
||||
rcfiles="$HOME/.profile"
|
||||
;;
|
||||
esac
|
||||
|
||||
for rc in $rcfiles; do
|
||||
[ -n "$rc" ] || continue
|
||||
if [ -f "$rc" ] && grep -qF "$marker_begin" "$rc" 2>/dev/null; then
|
||||
ok "PATH block already present in $(printf '%s' "$rc" | sed "s|^$HOME|~|")"
|
||||
continue
|
||||
fi
|
||||
mkdir -p "$(dirname "$rc")"
|
||||
if [ "$user_shell" = fish ]; then
|
||||
{
|
||||
printf '\n%s\n' "$marker_begin"
|
||||
printf 'fish_add_path -aP %s\n' "$install_dir"
|
||||
printf '%s\n' "$marker_end"
|
||||
} >> "$rc"
|
||||
else
|
||||
{
|
||||
printf '\n%s\n' "$marker_begin"
|
||||
# We want a literal $PATH in the rc file so the user's shell
|
||||
# expands it at source time, not us at install time.
|
||||
# shellcheck disable=SC2016
|
||||
printf 'export PATH="%s:$PATH"\n' "$install_dir"
|
||||
printf '%s\n' "$marker_end"
|
||||
} >> "$rc"
|
||||
fi
|
||||
ok "added $install_dir to PATH in $(printf '%s' "$rc" | sed "s|^$HOME|~|")"
|
||||
done
|
||||
}
|
||||
|
||||
# Homebrew handoff. Cask is macOS-only and brew handles its own PATH, so we
|
||||
# only divert when the user has plain `brew` on PATH on macOS, isn't pinning
|
||||
# a version, and didn't pick a custom install dir.
|
||||
should_use_brew() {
|
||||
[ "$1" = darwin ] || return 1
|
||||
[ -z "${GORTEX_NO_BREW:-}" ] || return 1
|
||||
[ "${GORTEX_VERSION:-latest}" = latest ] || return 1
|
||||
[ -z "${GORTEX_INSTALL_DIR:-}" ] || return 1
|
||||
command -v brew >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_via_brew() {
|
||||
if brew list --cask gortex >/dev/null 2>&1; then
|
||||
info "gortex cask already installed; upgrading via Homebrew"
|
||||
brew upgrade --cask gortex || die "brew upgrade failed"
|
||||
else
|
||||
info "installing via Homebrew (zzet/tap/gortex)"
|
||||
brew install zzet/tap/gortex || die "brew install failed"
|
||||
fi
|
||||
|
||||
bin="$(command -v gortex || true)"
|
||||
[ -n "$bin" ] || die "brew finished but 'gortex' is not on PATH"
|
||||
ok "installed $bin"
|
||||
|
||||
# Best-effort daemon restart, mirroring the tarball path.
|
||||
if "$bin" daemon status >/dev/null 2>&1; then
|
||||
info "restarting running daemon onto new binary"
|
||||
"$bin" daemon restart >/dev/null 2>&1 || warn "daemon restart failed; run 'gortex daemon restart' manually"
|
||||
fi
|
||||
|
||||
if version_out="$("$bin" version 2>/dev/null)"; then
|
||||
ok "$version_out"
|
||||
fi
|
||||
|
||||
printf '\n%sNext steps:%s\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf ' - %sgortex install%s one-time machine setup (MCP, skills, slash commands)\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf ' - %sgortex init%s run inside a repo to wire up your AI assistant\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf '\nDocs: https://github.com/%s\n\n' "$GORTEX_REPO"
|
||||
}
|
||||
|
||||
main() {
|
||||
os="$(detect_os)"
|
||||
arch="$(detect_arch)"
|
||||
version="${GORTEX_VERSION:-latest}"
|
||||
|
||||
if should_use_brew "$os"; then
|
||||
printf '\n%sGortex installer%s\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf ' os: %s\n' "$os"
|
||||
printf ' arch: %s\n' "$arch"
|
||||
printf ' version: %s (via Homebrew — set GORTEX_NO_BREW=1 to use the tarball)\n\n' "$version"
|
||||
install_via_brew
|
||||
return 0
|
||||
fi
|
||||
|
||||
install_dir="${GORTEX_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
|
||||
printf '\n%sGortex installer%s\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf ' os: %s\n' "$os"
|
||||
printf ' arch: %s\n' "$arch"
|
||||
printf ' version: %s\n' "$version"
|
||||
printf ' target: %s/%s\n\n' "$install_dir" "$GORTEX_BIN"
|
||||
|
||||
asset="gortex_${os}_${arch}.tar.gz"
|
||||
if [ "$version" = latest ]; then
|
||||
base_url="${DOWNLOAD_BASE}/latest/download"
|
||||
else
|
||||
case "$version" in
|
||||
v*) tag="$version" ;;
|
||||
*) tag="v$version" ;;
|
||||
esac
|
||||
base_url="${DOWNLOAD_BASE}/download/${tag}"
|
||||
fi
|
||||
asset_url="${base_url}/${asset}"
|
||||
checksums_url="${base_url}/checksums.txt"
|
||||
|
||||
tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t gortex-install)"
|
||||
# shellcheck disable=SC2064 # we want $tmpdir expanded now, not at trap time.
|
||||
trap "rm -rf '$tmpdir'" EXIT INT TERM
|
||||
|
||||
info "downloading $asset"
|
||||
http_get "$asset_url" "$tmpdir/$asset" \
|
||||
|| die "download failed: $asset_url"
|
||||
|
||||
if [ -z "${GORTEX_NO_VERIFY:-}" ]; then
|
||||
info "downloading checksums.txt"
|
||||
if http_get "$checksums_url" "$tmpdir/checksums.txt"; then
|
||||
# checksums.txt is `<sha256> <filename>` per goreleaser default.
|
||||
# Some versions prefix the filename with `*` for binary mode.
|
||||
expected="$(awk -v f="$asset" '$2==f || $2=="*"f {print $1; exit}' "$tmpdir/checksums.txt")"
|
||||
if [ -z "$expected" ]; then
|
||||
warn "checksums.txt did not contain $asset; skipping verification"
|
||||
else
|
||||
verify_sha256 "$tmpdir/$asset" "$expected"
|
||||
fi
|
||||
else
|
||||
warn "could not fetch checksums.txt; skipping verification"
|
||||
fi
|
||||
|
||||
# Optional cosign verification — only if cosign happens to be installed.
|
||||
# Releases ship .sig + .pem bound to GitHub Actions OIDC identity; see
|
||||
# README "Verifying releases" for the full standalone command.
|
||||
if command -v cosign >/dev/null 2>&1; then
|
||||
info "verifying cosign signature"
|
||||
if http_get "${asset_url}.sig" "$tmpdir/$asset.sig" \
|
||||
&& http_get "${asset_url}.pem" "$tmpdir/$asset.pem"; then
|
||||
if cosign verify-blob \
|
||||
--certificate "$tmpdir/$asset.pem" \
|
||||
--signature "$tmpdir/$asset.sig" \
|
||||
--certificate-identity-regexp 'https://github\.com/zzet/gortex/.*' \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
"$tmpdir/$asset" >/dev/null 2>&1; then
|
||||
ok "cosign verified"
|
||||
else
|
||||
die "cosign signature verification failed (set GORTEX_NO_VERIFY=1 to skip)"
|
||||
fi
|
||||
else
|
||||
warn "could not fetch cosign signature; skipping"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "verification disabled (GORTEX_NO_VERIFY)"
|
||||
fi
|
||||
|
||||
info "extracting"
|
||||
( cd "$tmpdir" && tar -xzf "$asset" )
|
||||
[ -f "$tmpdir/$GORTEX_BIN" ] || die "archive did not contain a $GORTEX_BIN binary"
|
||||
chmod +x "$tmpdir/$GORTEX_BIN"
|
||||
|
||||
mkdir -p "$install_dir" || die "cannot create $install_dir"
|
||||
# Probe writability before we trash a partial install.
|
||||
if ! ( : > "$install_dir/.gortex-write-test" ) 2>/dev/null; then
|
||||
die "no write permission to $install_dir
|
||||
Set GORTEX_INSTALL_DIR=\$HOME/.local/bin, or rerun with sudo if you really want a system path."
|
||||
fi
|
||||
rm -f "$install_dir/.gortex-write-test"
|
||||
|
||||
if [ -e "$install_dir/$GORTEX_BIN" ] && [ -z "${GORTEX_FORCE:-}" ]; then
|
||||
backup="$install_dir/${GORTEX_BIN}.previous"
|
||||
info "backing up existing binary to $backup"
|
||||
mv -f "$install_dir/$GORTEX_BIN" "$backup"
|
||||
fi
|
||||
mv -f "$tmpdir/$GORTEX_BIN" "$install_dir/$GORTEX_BIN"
|
||||
ok "installed $install_dir/$GORTEX_BIN"
|
||||
|
||||
# macOS Gatekeeper sets com.apple.quarantine on browser downloads. curl/wget
|
||||
# don't, but a re-run might inherit it from a prior browser fetch — strip
|
||||
# unconditionally; failure is harmless.
|
||||
if [ "$os" = darwin ] && command -v xattr >/dev/null 2>&1; then
|
||||
xattr -d com.apple.quarantine "$install_dir/$GORTEX_BIN" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -z "${GORTEX_NO_PATH:-}" ]; then
|
||||
update_path_in_rc "$install_dir"
|
||||
fi
|
||||
|
||||
# If the daemon is running an older binary, restart it on the new one.
|
||||
# Best-effort: never block install on this.
|
||||
if "$install_dir/$GORTEX_BIN" daemon status >/dev/null 2>&1; then
|
||||
info "restarting running daemon onto new binary"
|
||||
"$install_dir/$GORTEX_BIN" daemon restart >/dev/null 2>&1 || warn "daemon restart failed; run 'gortex daemon restart' manually"
|
||||
fi
|
||||
|
||||
# Print version banner. The binary lives at an absolute path so we don't
|
||||
# depend on PATH being refreshed in the current shell.
|
||||
if version_out="$("$install_dir/$GORTEX_BIN" version 2>/dev/null)"; then
|
||||
ok "$version_out"
|
||||
fi
|
||||
|
||||
printf '\n%sNext steps:%s\n' "${C_BOLD}" "${C_RESET}"
|
||||
case ":${PATH}:" in
|
||||
*":${install_dir}:"*) ;;
|
||||
*)
|
||||
printf ' 1. Open a new shell (or %ssource%s your rc file) so PATH picks up %s\n' "${C_DIM}" "${C_RESET}" "$install_dir"
|
||||
;;
|
||||
esac
|
||||
printf ' - %sgortex install%s one-time machine setup (MCP, skills, slash commands)\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf ' - %sgortex init%s run inside a repo to wire up your AI assistant\n' "${C_BOLD}" "${C_RESET}"
|
||||
printf '\nDocs: https://github.com/%s\n\n' "$GORTEX_REPO"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Regenerate docs/landing-pages/per-tool-savings.md from the
|
||||
# cumulative savings store + the JSONL event log. Three sections:
|
||||
#
|
||||
# 1. Headline totals (always populated — comes from the cumulative
|
||||
# store written every flush)
|
||||
# 2. Per-language breakdown (always populated — same source)
|
||||
# 3. Per-tool breakdown (populated once the JSONL event log has
|
||||
# accumulated calls; the JSONL surface was added 2026-05-18 so
|
||||
# stores predating that won't have per-tool data until enough
|
||||
# new sessions accumulate)
|
||||
#
|
||||
# Requires: gortex on PATH, jq.
|
||||
# Usage: bash scripts/landing/per-tool-savings.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${OUT:-docs/landing-pages/per-tool-savings.md}"
|
||||
SAVINGS_JSON="$(gortex savings --verbose --json)"
|
||||
|
||||
if ! echo "$SAVINGS_JSON" | jq -e '.calls_counted > 0' >/dev/null; then
|
||||
echo "per-tool-savings: cumulative savings store is empty" >&2
|
||||
echo " (use gortex via MCP for a while, then re-run)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
TOTAL_CALLS="$(echo "$SAVINGS_JSON" | jq -r '.calls_counted')"
|
||||
TOTAL_SAVED="$(echo "$SAVINGS_JSON" | jq -r '.tokens_saved')"
|
||||
TOTAL_RETURNED="$(echo "$SAVINGS_JSON" | jq -r '.tokens_returned')"
|
||||
HEADLINE_COST_OPUS="$(echo "$SAVINGS_JSON" | jq -r '.cost_avoided_usd["claude-opus-4-8"]')"
|
||||
HEADLINE_COST_SONNET="$(echo "$SAVINGS_JSON" | jq -r '.cost_avoided_usd["claude-sonnet-4-6"]')"
|
||||
HEADLINE_COST_HAIKU="$(echo "$SAVINGS_JSON" | jq -r '.cost_avoided_usd["claude-haiku-4-5"]')"
|
||||
HEADLINE_COST_GPT4O="$(echo "$SAVINGS_JSON" | jq -r '.cost_avoided_usd["gpt-4o"]')"
|
||||
EFFICIENCY="$(echo "$SAVINGS_JSON" | jq -r 'if .tokens_returned > 0 then ((.tokens_saved + .tokens_returned) / .tokens_returned) else 0 end')"
|
||||
|
||||
cat > "$OUT" <<EOF
|
||||
# Gortex token savings — published landing-page tables
|
||||
|
||||
**Last regenerated**: $generated_at · Source: \`gortex savings
|
||||
--verbose --json\` against the operator's cumulative store
|
||||
(the \`~/.gortex/sidecar.sqlite\` savings ledger).
|
||||
|
||||
## Headline
|
||||
|
||||
| metric | value |
|
||||
|--------|------:|
|
||||
| Source-reading MCP calls | **$TOTAL_CALLS** |
|
||||
| Tokens saved (vs naive file reads) | **$TOTAL_SAVED** |
|
||||
| Tokens returned | $TOTAL_RETURNED |
|
||||
| Efficiency ratio | $(printf '%.1fx' "$EFFICIENCY") |
|
||||
| USD avoided · claude-opus-4-8 | **\$$(printf '%.2f' "$HEADLINE_COST_OPUS")** |
|
||||
| USD avoided · claude-sonnet-4-6 | \$$(printf '%.2f' "$HEADLINE_COST_SONNET") |
|
||||
| USD avoided · claude-haiku-4-5 | \$$(printf '%.2f' "$HEADLINE_COST_HAIKU") |
|
||||
| USD avoided · gpt-4o | \$$(printf '%.2f' "$HEADLINE_COST_GPT4O") |
|
||||
|
||||
## By language
|
||||
|
||||
| language | calls | tokens saved | savings % |
|
||||
|----------|------:|-------------:|----------:|
|
||||
EOF
|
||||
|
||||
echo "$SAVINGS_JSON" | jq -r '
|
||||
.per_language | to_entries |
|
||||
sort_by(-.value.tokens_saved) |
|
||||
.[] |
|
||||
[.key, .value.calls_counted, .value.tokens_saved,
|
||||
(if (.value.tokens_saved + .value.tokens_returned) > 0
|
||||
then ((.value.tokens_saved / (.value.tokens_saved + .value.tokens_returned)) * 100 | floor)
|
||||
else 0 end)] |
|
||||
"| \(.[0]) | \(.[1]) | \(.[2]) | \(.[3])% |"
|
||||
' >> "$OUT"
|
||||
|
||||
cat >> "$OUT" <<EOF
|
||||
|
||||
## By repository
|
||||
|
||||
| repo | calls | tokens saved |
|
||||
|------|------:|-------------:|
|
||||
EOF
|
||||
|
||||
echo "$SAVINGS_JSON" | jq -r '
|
||||
.per_repo | to_entries |
|
||||
sort_by(-.value.tokens_saved) |
|
||||
.[] |
|
||||
[.key, .value.calls_counted, .value.tokens_saved] |
|
||||
"| \(.[0]) | \(.[1]) | \(.[2]) |"
|
||||
' >> "$OUT"
|
||||
|
||||
# Per-tool section: only populated when the JSONL event log has
|
||||
# rows for the All-time bucket. Stores predating the JSONL surface
|
||||
# (added 2026-05-18) will skip this section until enough new
|
||||
# sessions accumulate. Honest about the gap rather than padding
|
||||
# with synthetic numbers.
|
||||
PER_TOOL_COUNT="$(echo "$SAVINGS_JSON" | jq -r '.buckets[2].per_tool | length // 0')"
|
||||
|
||||
cat >> "$OUT" <<EOF
|
||||
|
||||
## By MCP tool
|
||||
EOF
|
||||
|
||||
if [[ "$PER_TOOL_COUNT" -gt 0 ]]; then
|
||||
cat >> "$OUT" <<EOF
|
||||
|
||||
| tool | calls | tokens saved | median saved/call |
|
||||
|------|------:|-------------:|------------------:|
|
||||
EOF
|
||||
echo "$SAVINGS_JSON" | jq -r '
|
||||
.buckets[2].per_tool |
|
||||
sort_by(-.tokens_saved) |
|
||||
.[] |
|
||||
[.tool, .calls_counted, .tokens_saved,
|
||||
(if .calls_counted > 0 then (.tokens_saved / .calls_counted | floor) else 0 end)] |
|
||||
"| \(.[0]) | \(.[1]) | \(.[2]) | \(.[3]) |"
|
||||
' >> "$OUT"
|
||||
else
|
||||
cat >> "$OUT" <<'EOF'
|
||||
|
||||
_Per-tool breakdown becomes available once the per-call JSONL
|
||||
event log accumulates rows. The JSONL surface was added 2026-05-18;
|
||||
cumulative stores predating that version won't show per-tool data
|
||||
until enough new sessions populate the log. Re-run this script
|
||||
after a week of usage to surface the table here._
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >> "$OUT" <<'EOF'
|
||||
|
||||
## Reproducing this page
|
||||
|
||||
```sh
|
||||
bash scripts/landing/per-tool-savings.sh
|
||||
```
|
||||
|
||||
The script reads from the cumulative savings store + JSONL event
|
||||
log written by the MCP server every time a source-reading tool
|
||||
returns. See `cmd/gortex/savings.go` for the dashboard the script
|
||||
wraps, and `internal/savings/` for the persistence layer.
|
||||
|
||||
Override pricing for org-specific rates by exporting
|
||||
`GORTEX_MODEL_PRICING_JSON` before running.
|
||||
EOF
|
||||
|
||||
echo "wrote $OUT" >&2
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare GloVe word vectors for the StaticProvider.
|
||||
# Downloads GloVe 6B, extracts top 100k words (300d), converts to binary format.
|
||||
#
|
||||
# Usage: ./scripts/prepare_vectors.sh
|
||||
#
|
||||
# Output: internal/embedding/data/vectors.bin.gz (~3MB)
|
||||
# Input: Downloads glove.6B.zip (~862MB) to /tmp/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GLOVE_URL="https://nlp.stanford.edu/data/glove.6B.zip"
|
||||
GLOVE_DIR="/tmp/glove"
|
||||
OUTPUT_DIR="internal/embedding/data"
|
||||
OUTPUT_FILE="${OUTPUT_DIR}/vectors.bin.gz"
|
||||
MAX_WORDS=20000
|
||||
DIMS=50
|
||||
|
||||
echo "=== GloVe Vector Preparation ==="
|
||||
|
||||
# Download if not cached.
|
||||
if [ ! -f "${GLOVE_DIR}/glove.6B.${DIMS}d.txt" ]; then
|
||||
echo "Downloading GloVe 6B..."
|
||||
mkdir -p "${GLOVE_DIR}"
|
||||
curl -L -o "${GLOVE_DIR}/glove.6B.zip" "${GLOVE_URL}"
|
||||
echo "Extracting..."
|
||||
unzip -o "${GLOVE_DIR}/glove.6B.zip" "glove.6B.${DIMS}d.txt" -d "${GLOVE_DIR}"
|
||||
fi
|
||||
|
||||
echo "Converting to binary format (top ${MAX_WORDS} words, ${DIMS}d)..."
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
python3 -c "
|
||||
import struct, gzip, sys
|
||||
|
||||
input_file = '${GLOVE_DIR}/glove.6B.${DIMS}d.txt'
|
||||
output_file = '${OUTPUT_FILE}'
|
||||
max_words = ${MAX_WORDS}
|
||||
dims = ${DIMS}
|
||||
|
||||
words = []
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
for i, line in enumerate(f):
|
||||
if i >= max_words:
|
||||
break
|
||||
parts = line.strip().split(' ')
|
||||
word = parts[0]
|
||||
vec = [float(x) for x in parts[1:]]
|
||||
if len(vec) != dims:
|
||||
continue
|
||||
words.append((word, vec))
|
||||
|
||||
print(f'Loaded {len(words)} words')
|
||||
|
||||
with gzip.open(output_file, 'wb') as f:
|
||||
# Header: word count + dimensions
|
||||
f.write(struct.pack('<II', len(words), dims))
|
||||
for word, vec in words:
|
||||
word_bytes = word.encode('utf-8')
|
||||
f.write(struct.pack('<H', len(word_bytes)))
|
||||
f.write(word_bytes)
|
||||
f.write(struct.pack(f'<{dims}f', *vec))
|
||||
|
||||
import os
|
||||
size_mb = os.path.getsize(output_file) / (1024 * 1024)
|
||||
print(f'Written {output_file} ({size_mb:.1f} MB)')
|
||||
"
|
||||
|
||||
echo "Done. Add the following to static_data.go:"
|
||||
echo ' //go:embed data/vectors.bin.gz'
|
||||
echo ' var vectorData []byte'
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Goreleaser build hook: codesigns darwin Mach-O binaries with rcodesign
|
||||
# so they survive macOS Gatekeeper / notary checks. No-op on non-darwin
|
||||
# targets — the hook fires for every {goos, goarch} pair, and we gate on
|
||||
# the first arg ($1 = template "{{ .Os }}").
|
||||
#
|
||||
# Expects the workflow step to have populated MACOS_SIGNING_DIR with:
|
||||
# rcodesign — apple-codesign linux-musl binary
|
||||
# cert.p12 — Developer ID Application certificate
|
||||
# cert.pass — P12 export password (newline-free)
|
||||
set -eu
|
||||
|
||||
[ "$1" = darwin ] || exit 0
|
||||
|
||||
SIGNING_DIR="${MACOS_SIGNING_DIR:-/tmp/macos-signing}"
|
||||
|
||||
exec "$SIGNING_DIR/rcodesign" sign \
|
||||
--p12-file "$SIGNING_DIR/cert.p12" \
|
||||
--p12-password-file "$SIGNING_DIR/cert.pass" \
|
||||
--code-signature-flags runtime \
|
||||
"$2"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
# Smoke test for the macOS __DATA_CONST regression (issue #176).
|
||||
#
|
||||
# macOS 15 Sequoia and 26 Tahoe's dyld ABORT a process at load time if the
|
||||
# __DATA_CONST segment is missing the SG_READ_ONLY (0x10) segment flag:
|
||||
#
|
||||
# dyld: __DATA_CONST segment missing SG_READ_ONLY flag in <binary>
|
||||
#
|
||||
# Apple's native linker (ld-prime) sets that flag; the osxcross/cctools ld64
|
||||
# used by goreleaser-cross did not — so a cross-compiled cask shipped broken
|
||||
# while every locally `go build`-ed binary worked. This guard fails the
|
||||
# release if a darwin binary is produced without the flag, so the broken
|
||||
# shape can never ship again.
|
||||
#
|
||||
# Usage: verify-macho-readonly.sh <mach-o-binary>
|
||||
set -eu
|
||||
|
||||
bin="${1:?usage: verify-macho-readonly.sh <mach-o-binary>}"
|
||||
[ -f "$bin" ] || { echo "FATAL: $bin does not exist" >&2; exit 1; }
|
||||
|
||||
# Pull the SEGMENT-level flags for __DATA_CONST out of `otool -l`. A segment
|
||||
# block is "cmd LC_SEGMENT_64 ... segname __DATA_CONST ... flags 0xNN"; the
|
||||
# section sub-blocks that follow also carry a segname field, so we capture the
|
||||
# FIRST flags line after the segment's segname and stop — that is the segment
|
||||
# command's flags, not a section's.
|
||||
flags="$(otool -l "$bin" | awk '
|
||||
/^Load command/ { inseg=0; seg="" }
|
||||
$1=="cmd" && ($2=="LC_SEGMENT_64" || $2=="LC_SEGMENT") { inseg=1 }
|
||||
inseg && $1=="segname" && seg=="" { seg=$2 }
|
||||
inseg && $1=="flags" && seg=="__DATA_CONST" { print $2; exit }
|
||||
')"
|
||||
|
||||
if [ -z "$flags" ]; then
|
||||
echo "FATAL: no __DATA_CONST segment found in $bin (is it a Mach-O?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# otool prints segment flags as hex (e.g. 0x10 / 0x00000010); some builds
|
||||
# render the symbolic name. Accept either form.
|
||||
case "$flags" in
|
||||
*READ_ONLY*) echo "ok: $bin __DATA_CONST carries SG_READ_ONLY ($flags)"; exit 0 ;;
|
||||
esac
|
||||
|
||||
# Shell arithmetic understands the 0x prefix; SG_READ_ONLY is 0x10.
|
||||
if [ $(( flags & 0x10 )) -eq 0 ]; then
|
||||
echo "FATAL: $bin __DATA_CONST flags=$flags missing SG_READ_ONLY (0x10)." >&2
|
||||
echo " This binary would abort under the macOS 15+/Tahoe dyld (issue #176)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ok: $bin __DATA_CONST carries SG_READ_ONLY (flags=$flags)"
|
||||
Reference in New Issue
Block a user