Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ jobs:
files: coverage.out
continue-on-error: true

build-windows:
# CGO build smoke-test on native Windows. tree-sitter needs a C/C++
# compiler — the GitHub windows runner ships mingw-w64 on PATH, so no
# extra toolchain setup is required. Build-only: `go build ./...`
# compiles every production package without pulling in the
# platform-specific test files a full `go test` would.
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: '1.26'

- name: Build CLI
run: go build -o gortex.exe ./cmd/gortex/

- name: Build all packages
run: go build ./...

lint:
runs-on: ubuntu-latest
steps:
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/install-script.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ on:
pull_request:
paths:
- "scripts/install.sh"
- "scripts/install.ps1"
- ".github/workflows/install-script.yml"
push:
branches: [main]
paths:
- "scripts/install.sh"
- "scripts/install.ps1"

jobs:
posix-syntax:
Expand Down Expand Up @@ -77,3 +79,20 @@ jobs:
# Marker block must appear exactly once.
count=$(grep -c '^# >>> gortex installer >>>' "$HOME/.bashrc")
test "$count" = "1"

powershell-syntax:
# Parse-check install.ps1 — the PowerShell counterpart of the
# posix-syntax job. A full install can only run once a release ships
# Windows artifacts, so this validates that the script parses without
# errors on every change.
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Parse install.ps1
shell: pwsh
run: |
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path scripts/install.ps1), [ref]$null, [ref]$errors) | Out-Null
if ($errors) { $errors; exit 1 }
Write-Host "install.ps1 parses cleanly"
11 changes: 9 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,18 @@ jobs:
-w /go/src/gortex \
-e GITHUB_TOKEN \
-e HOMEBREW_TAP_TOKEN \
-e SCOOP_BUCKET_TOKEN \
ghcr.io/goreleaser/goreleaser-cross:v1.26 \
release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Personal access token with `repo` scope on zzet/homebrew-tap.
# GITHUB_TOKEN can only push to the source repo, not the tap.
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
# Same story for the Scoop bucket — a PAT with `repo` scope on
# gortexhq/scoop-bucket. Required for the `scoops` block in
# .goreleaser.yml to push the Windows manifest.
SCOOP_BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }}

# goreleaser-cross runs as root inside the container, so everything
# in dist/ is owned by root:root on the host. The subsequent cosign
Expand Down Expand Up @@ -155,7 +160,7 @@ jobs:
run: |
cd dist
shopt -s nullglob
for f in *.tar.gz *.deb *.rpm *.apk checksums.txt; do
for f in *.tar.gz *.zip *.deb *.rpm *.apk checksums.txt; do
echo "Signing $f..."
cosign sign-blob \
--output-signature "${f}.sig" \
Expand Down Expand Up @@ -189,7 +194,7 @@ jobs:
# (one "hash filename" line per artifact). We hash the primary
# artifacts only — .sig/.pem are attestations of these files,
# they don't need their own provenance.
HASHES=$(sha256sum *.tar.gz *.deb *.rpm *.apk | base64 -w0)
HASHES=$(sha256sum *.tar.gz *.zip *.deb *.rpm *.apk | base64 -w0)
echo "hashes=$HASHES" >> "$GITHUB_OUTPUT"

# Always wipe signing material — even on failure — so a P12 / .p8
Expand Down Expand Up @@ -241,6 +246,7 @@ jobs:
mkdir -p dist
gh release download "${GITHUB_REF#refs/tags/}" --dir dist \
--pattern '*.tar.gz' \
--pattern '*.zip' \
--pattern '*.deb' \
--pattern '*.rpm' \
--pattern '*.apk'
Expand All @@ -251,6 +257,7 @@ jobs:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
./dist/*.tar.gz
./dist/*.zip
./dist/*.deb
./dist/*.rpm
./dist/*.apk
Expand Down
36 changes: 36 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ builds:
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ignore:
# windows/arm64 needs an aarch64-w64-mingw32 cross-toolchain that
# the goreleaser-cross image doesn't ship; windows/amd64 covers
# every mainstream Windows dev box. Revisit when the image gains
# the llvm-mingw arm64 target.
- goos: windows
goarch: arm64
# Per-target CC + CXX. goreleaser-cross exposes these cross-toolchains
# on PATH; CGO needs both set per target triple because some deps
# (tree-sitter yaml scanner, etc.) ship C++. Without CXX, the system
Expand All @@ -53,6 +61,11 @@ builds:
env:
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
- goos: windows
goarch: amd64
env:
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
# Per-target build hook. Fires after each Mach-O / ELF is linked,
# before the archive step. The script is a no-op for non-darwin
# targets, so we don't need a per-override hook list.
Expand All @@ -66,6 +79,11 @@ builds:

archives:
- formats: [tar.gz]
# Windows users expect a .zip, not a .tar.gz — and Scoop only
# understands zip archives.
format_overrides:
- goos: windows
formats: [zip]
# Version intentionally omitted — `/releases/latest/download/<name>` URLs
# require an exact filename, and users linking to the latest release
# shouldn't need to know the version. Version is still in the release
Expand Down Expand Up @@ -124,3 +142,21 @@ homebrew_casks:
generate_completions_from_executable:
executable: gortex
shell_parameter_format: cobra

# Scoop manifest — `scoop install gortex` on Windows. goreleaser commits
# the generated manifest (pointing at the signed windows/amd64 .zip in
# this release) to a separate bucket repo on every tagged release,
# exactly like the Homebrew cask above.
scoops:
- name: gortex
repository:
owner: gortexhq
name: scoop-bucket
# GITHUB_TOKEN can only push to the source repo, so the bucket
# needs its own PAT with `repo` scope on gortexhq/scoop-bucket,
# stored as SCOOP_BUCKET_TOKEN in repo secrets. release.yml wires
# it in.
token: "{{ .Env.SCOOP_BUCKET_TOKEN }}"
homepage: "https://github.com/zzet/gortex"
description: "Code intelligence engine that indexes repositories into an in-memory knowledge graph."
license: "Custom"
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)

.PHONY: build build-onnx build-gomlx build-hugot \
.PHONY: build build-onnx build-gomlx build-hugot build-windows \
test bench bench-rpi bench-rpi-quick bench-rpi-profile bench-compare \
lint fmt clean install tag-release \
deps-onnx deps-gomlx deps-hugot deps-vectors \
Expand Down Expand Up @@ -73,7 +73,7 @@ fmt:
gofmt -s -w .

clean:
rm -f $(BINARY) gortex-linux gortex-rpi
rm -f $(BINARY) gortex.exe gortex-linux gortex-rpi gortex-rpi32

install:
go install -ldflags '$(LDFLAGS)' ./cmd/gortex/
Expand Down Expand Up @@ -118,6 +118,17 @@ build-rpi32:
go build -ldflags '$(LDFLAGS)' -o gortex-rpi32 ./cmd/gortex/
@echo "✓ Built gortex-rpi32 (linux/arm/v7)"

# Cross-compile for Windows (amd64). Requires the mingw-w64 toolchain
# (`brew install mingw-w64` on macOS, `apt install gcc-mingw-w64` on
# Debian/Ubuntu). CGO stays on because tree-sitter needs a C/C++
# compiler; the llama tag is omitted — the in-process llama.cpp backend
# isn't part of the Windows build.
build-windows:
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 \
CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ \
go build -ldflags '$(LDFLAGS)' -o gortex.exe ./cmd/gortex/
@echo "✓ Built gortex.exe (windows/amd64)"

# ---------------------------------------------------------------------------
# Marketplace plugin bundle
# ---------------------------------------------------------------------------
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

[![macOS](https://img.shields.io/badge/macOS-supported-blue.svg)](#)
[![Linux](https://img.shields.io/badge/Linux-supported-blue.svg)](#)
[![Windows](https://img.shields.io/badge/Windows-supported-blue.svg)](#)

[![Claude Code](https://img.shields.io/badge/Claude_Code-supported-blueviolet.svg)](#)
[![Cursor](https://img.shields.io/badge/Cursor-supported-blueviolet.svg)](#)
Expand All @@ -44,10 +45,16 @@ Built for 15 AI coding agents (Claude Code, Kiro, Cursor, Windsurf, VS Code (and
## Installation

```bash
# macOS / Linux
curl -fsSL https://get.gortex.dev | sh
```

> Detects OS/arch, downloads the signed release tarball, verifies the SHA256 against `checksums.txt` (and cosign if installed), drops the binary in `$HOME/.local/bin`, and adds it to your shell rc. Re-runs upgrade in place. No silent sudo. Linux + macOS, amd64 + arm64. Windows support is planned.
```powershell
# Windows (PowerShell)
irm https://get.gortex.dev/install.ps1 | iex
```

> Detects OS/arch, downloads the signed release archive, verifies the SHA256 against `checksums.txt` (and cosign if installed), installs the binary, and puts it on your PATH. Re-runs upgrade in place. No silent sudo. Linux + macOS + Windows, amd64 + arm64. On Windows, `scoop install gortex` works too.

For Homebrew, package managers (`.deb` / `.rpm` / `.apk`), direct binary download, supply-chain verification (cosign + SLSA-3 + VirusTotal), and from-source builds — see [docs/installation.md](docs/installation.md).

Expand Down
40 changes: 23 additions & 17 deletions cmd/gortex/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"sort"
"strconv"
"strings"
"syscall"
"time"

"github.com/jedib0t/go-pretty/v6/table"
Expand All @@ -21,6 +20,7 @@ import (
"github.com/zzet/gortex/internal/daemon"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/indexer"
"github.com/zzet/gortex/internal/platform"
)

var (
Expand Down Expand Up @@ -82,7 +82,7 @@ var daemonLogsCmd = &cobra.Command{

func init() {
daemonStartCmd.Flags().BoolVar(&daemonDetach, "detach", false,
"fork to background after starting (writes to ~/.cache/gortex/daemon.log)")
"fork to background after starting (logs to the daemon log file — see `gortex daemon logs`)")
daemonStartCmd.Flags().BoolVar(&daemonEmbeddings, "embeddings", false,
"load a semantic embedding provider (opt-in — adds ~87 MB model download on first use and ~60 ms/symbol warmup)")
daemonStartCmd.Flags().StringVar(&daemonHTTPAddr, "http-addr", "",
Expand Down Expand Up @@ -459,14 +459,18 @@ func spawnDetachedDaemon() error {
child.Stdout = logFile
child.Stderr = logFile
child.Stdin = nil
// Detach from the controlling terminal so Ctrl-C on the parent
// doesn't kill the daemon.
child.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
// Detach the child from the parent's controlling terminal /
// console so Ctrl-C on the parent doesn't kill the daemon.
child.SysProcAttr = platform.DetachSysProcAttr()
if err := child.Start(); err != nil {
_ = logFile.Close()
return fmt.Errorf("spawn daemon: %w", err)
}
// Don't wait — the child inherits the log file handle.
// Don't block on the child — it's detached and inherits the log
// file handle. Reap it in a background goroutine so a crash during
// startup surfaces on `exited` instead of stalling the poll loop.
exited := make(chan error, 1)
go func() { exited <- child.Wait() }()

// Wait until the socket is live or a timeout hits, so we fail fast
// if the child died on startup. The socket opens after buildDaemonState
Expand All @@ -484,10 +488,11 @@ func spawnDetachedDaemon() error {
}
// Bail out early if the child has already exited — no point
// waiting another 59 seconds for a corpse.
var ws syscall.WaitStatus
if pid, _ := syscall.Wait4(child.Process.Pid, &ws, syscall.WNOHANG, nil); pid == child.Process.Pid {
return fmt.Errorf("daemon exited during startup (status %d); check %s",
ws.ExitStatus(), daemon.LogFilePath())
select {
case werr := <-exited:
return fmt.Errorf("daemon exited during startup (%v); check %s",
werr, daemon.LogFilePath())
default:
}
time.Sleep(50 * time.Millisecond)
}
Expand Down Expand Up @@ -926,8 +931,9 @@ func daemonControlClient() (*daemon.Client, error) {
}

// killByPID is the fallback stop path for stale daemons that have a PID
// file but don't respond on the socket. Sends SIGTERM, waits, then
// SIGKILL. Silently returns nil if the PID no longer exists.
// file but don't respond on the socket. Asks the process to terminate,
// waits, then force-kills. Silently returns nil if the PID no longer
// exists.
func killByPID() error {
pidBytes, err := os.ReadFile(daemon.PIDFilePath())
if err != nil {
Expand All @@ -937,24 +943,24 @@ func killByPID() error {
if pid <= 0 {
return nil
}
_ = syscall.Kill(pid, syscall.SIGTERM)
_ = platform.TerminateProcess(pid)

deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if err := syscall.Kill(pid, 0); err != nil {
if !platform.ProcessAlive(pid) {
// Process gone.
_ = os.Remove(daemon.PIDFilePath())
_ = os.Remove(daemon.SocketPath())
fmt.Fprintln(os.Stderr, "[gortex daemon] stopped (via SIGTERM)")
fmt.Fprintln(os.Stderr, "[gortex daemon] stopped")
return nil
}
time.Sleep(100 * time.Millisecond)
}
// Last resort.
_ = syscall.Kill(pid, syscall.SIGKILL)
_ = platform.KillProcess(pid)
_ = os.Remove(daemon.PIDFilePath())
_ = os.Remove(daemon.SocketPath())
fmt.Fprintln(os.Stderr, "[gortex daemon] stopped (via SIGKILL)")
fmt.Fprintln(os.Stderr, "[gortex daemon] stopped (force-killed)")
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/gortex/daemon_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func runDaemonInstallService(cmd *cobra.Command, _ []string) error {
case "linux":
return installSystemd(w, exe)
default:
return fmt.Errorf("service install not supported on %s yet (Windows is Phase 4+, contributions welcome)", runtime.GOOS)
return fmt.Errorf("service install is not supported on %s — run 'gortex daemon start --detach' to keep the daemon running in the background", runtime.GOOS)
}
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/gortex/eval_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/spf13/cobra"

Expand All @@ -17,6 +16,7 @@ import (
gortexmcp "github.com/zzet/gortex/internal/mcp"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/parser/languages"
"github.com/zzet/gortex/internal/platform"
"github.com/zzet/gortex/internal/query"
)

Expand Down Expand Up @@ -131,7 +131,7 @@ func runEvalServer(cmd *cobra.Command, args []string) error {

// Handle graceful shutdown.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(sigCh, platform.ShutdownSignals()...)

select {
case err := <-errCh:
Expand Down
Loading
Loading