Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4c0124b
feat: add native Windows support for Zeude CLI
scm1400 Mar 6, 2026
a23433b
feat(hooks): complete Windows hook compatibility with dual .sh/.ps1 g…
scm1400 Mar 17, 2026
b3e6b67
build: add lightweight Dockerfile.releases for install testing
scm1400 Mar 17, 2026
d660a7d
fix(installer): add Wait-AndExit to prevent PowerShell window from cl…
scm1400 Mar 17, 2026
4450952
debug(installer): show actual error detail on download failure
scm1400 Mar 17, 2026
1895bc9
fix(installer): handle locked exe by rename-before-replace on Windows
scm1400 Mar 17, 2026
e550f8c
fix(installer): remove pause on successful install, keep only on errors
scm1400 Mar 17, 2026
75f388c
feat(build): replace placeholder URLs at build time via APP_URL/OTEL_…
scm1400 Mar 17, 2026
42031ae
feat(build): bake actual deployment URLs into install scripts
scm1400 Mar 17, 2026
2a1cd6f
fix(installer): add explicit auth URL pattern to /zeude skill
scm1400 Mar 17, 2026
6ff4194
build: use placeholder defaults for APP_URL/OTEL_URL build args
scm1400 Mar 17, 2026
88ec3b4
chore: remove Dockerfile.releases (only needed for deployment testing)
scm1400 Mar 17, 2026
8f0d39d
fix(installer): detect Windows Git Bash and redirect to PowerShell in…
scm1400 Mar 17, 2026
ee78116
fix(installer): exit main script when detect_platform fails in subshell
scm1400 Mar 17, 2026
4f04ae4
fix(installer): detect Windows at script start before any functions
scm1400 Mar 17, 2026
337d760
chore(installer): remove redundant Windows detection from detect_plat…
scm1400 Mar 17, 2026
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ This proves that **"Measurement brings visibility, and sharing drives adoption."
- [Claude Code](https://www.anthropic.com/claude-code) installed
- [Supabase](https://supabase.com) account (for data persistence)
- [ClickHouse](https://clickhouse.com) instance (for analytics)
- macOS (Intel/Apple Silicon) or Linux (x86_64/arm64)
- macOS (Intel/Apple Silicon), Linux (x86_64/arm64), or Windows (x86_64/arm64)

### Installation

Expand Down Expand Up @@ -186,10 +186,17 @@ This proves that **"Measurement brings visibility, and sharing drives adoption."
```

5. **Install CLI on client machines**

**macOS/Linux:**
```bash
curl -fsSL https://your-dashboard-url/releases/install.sh | ZEUDE_AGENT_KEY=zd_xxx bash
```

**Windows (PowerShell):**
```powershell
$env:ZEUDE_AGENT_KEY="zd_xxx"; irm https://your-dashboard-url/releases/install.ps1 | iex
```

---

## How It Works
Expand Down
12 changes: 11 additions & 1 deletion zeude/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@ RUN VERSION=$(cat /tmp/version) && \
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o /releases/zeude-darwin-arm64 ./cmd/doctor && \
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /releases/zeude-linux-amd64 ./cmd/doctor && \
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o /releases/zeude-linux-arm64 ./cmd/doctor && \
GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o /releases/claude-windows-amd64.exe ./cmd/claude && \
GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o /releases/claude-windows-arm64.exe ./cmd/claude && \
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o /releases/zeude-windows-amd64.exe ./cmd/doctor && \
GOOS=windows GOARCH=arm64 go build -ldflags="-s -w" -o /releases/zeude-windows-arm64.exe ./cmd/doctor && \
echo "$VERSION" > /releases/version.txt

# Copy install and uninstall scripts
# Copy install/uninstall scripts and replace placeholder URLs
ARG APP_URL=https://your-dashboard-url
ARG OTEL_URL=https://your-otel-collector-url/
COPY scripts/install.sh /releases/install.sh
COPY scripts/uninstall.sh /releases/uninstall.sh
COPY scripts/install.ps1 /releases/install.ps1
COPY scripts/uninstall.ps1 /releases/uninstall.ps1
RUN sed -i "s|https://your-dashboard-url|${APP_URL}|g" /releases/*.sh /releases/*.ps1 && \
sed -i "s|https://your-otel-collector-url/|${OTEL_URL}|g" /releases/*.sh /releases/*.ps1

# Node build stage
FROM node:20-alpine AS builder
Expand Down
22 changes: 21 additions & 1 deletion zeude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ curl -fsSL https://your-dashboard-url/releases/install.sh | ZEUDE_AGENT_KEY=zd_x

Replace `zd_xxx` with your agent key from the dashboard.

## Quick Install (Windows)

```powershell
$env:ZEUDE_AGENT_KEY="zd_xxx"; irm https://your-dashboard-url/releases/install.ps1 | iex
```

Replace `zd_xxx` with your agent key from the dashboard.

## Manual Installation

### Prerequisites

- Claude Code installed (`npm install -g @anthropic-ai/claude-code`)
- macOS (Intel/Apple Silicon) or Linux (x86_64/arm64)
- macOS (Intel/Apple Silicon), Linux (x86_64/arm64), or Windows (x86_64/arm64)

### Steps

Expand Down Expand Up @@ -215,6 +223,18 @@ This will:
cat ~/.zeude/config-cache.json | jq '.config.serverCount'
```

### Windows: PATH not updated

1. Open a NEW terminal after installation (PATH changes require restart)
2. Verify PATH:
```powershell
$env:PATH -split ";" | Select-String "zeude"
```
3. Manual fix:
```powershell
[Environment]::SetEnvironmentVariable("PATH", "$env:USERPROFILE\.zeude\bin;$([Environment]::GetEnvironmentVariable('PATH','User'))", "User")
```

### Real Claude not found

The shim couldn't find the original Claude CLI. Ensure it's installed:
Expand Down
9 changes: 6 additions & 3 deletions zeude/cmd/claude/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"os"
"strings"
"sync"
"syscall"

"github.com/zeude/zeude/internal/autoupdate"
"github.com/zeude/zeude/internal/executil"
"github.com/zeude/zeude/internal/config"
"github.com/zeude/zeude/internal/mcpconfig"
"github.com/zeude/zeude/internal/resolver"
Expand Down Expand Up @@ -120,8 +120,11 @@ func main() {
// 6. Inject telemetry environment variables (only if not already set)
injectTelemetryEnv(syncResult)

// 7. Exec real claude (replaces this process - no PTY needed!)
err = syscall.Exec(realClaude, os.Args, os.Environ())
// 7. Mark as initialized so SessionStart hooks can skip re-init
os.Setenv("ZEUDE_INITIALIZED", "1")

// 8. Exec real claude (replaces this process - no PTY needed!)
err = executil.Exec(realClaude, os.Args, os.Environ())
if err != nil {
fmt.Fprintf(os.Stderr, "zeude: failed to exec claude: %v\n", err)
os.Exit(1)
Expand Down
9 changes: 7 additions & 2 deletions zeude/cmd/doctor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -81,9 +82,13 @@ func checkShimInstalled() checkResult {
return checkResult{"Shim installed", "fail", "Cannot get home directory"}
}

shimPath := filepath.Join(home, ".zeude", "bin", "claude")
shimName := "claude"
if runtime.GOOS == "windows" {
shimName = "claude.exe"
}
shimPath := filepath.Join(home, ".zeude", "bin", shimName)
if _, err := os.Stat(shimPath); os.IsNotExist(err) {
return checkResult{"Shim installed", "fail", "Shim not found at ~/.zeude/bin/claude"}
return checkResult{"Shim installed", "fail", fmt.Sprintf("Shim not found at ~/.zeude/bin/%s", shimName)}
}

return checkResult{"Shim installed", "pass", shimPath}
Expand Down
95 changes: 88 additions & 7 deletions zeude/cmd/zeude/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"runtime"
"strings"
"sync"

"github.com/zeude/zeude/internal/autoupdate"
"github.com/zeude/zeude/internal/executil"
"github.com/zeude/zeude/internal/mcpconfig"
)

const (
Expand All @@ -27,6 +32,8 @@ func main() {
}

switch os.Args[1] {
case "init":
runInit()
case "update":
runUpdate()
case "doctor":
Expand All @@ -48,6 +55,7 @@ func printUsage() {
fmt.Println("Usage: zeude <command>")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" init Run initialization (MCP sync + update check)")
fmt.Println(" update Check for updates and install if available")
fmt.Println(" doctor Run diagnostic checks")
fmt.Println(" version Show version information")
Expand Down Expand Up @@ -82,6 +90,61 @@ func runUpdate() {
}
}

// runInit performs the same initialization as the claude shim (MCP sync + update check)
// but without exec'ing the real claude binary. Designed to be called from SessionStart hooks
// so that Claude Code Desktop app sessions also get Zeude initialization.
func runInit() {
var updateResult autoupdate.UpdateResult
var syncResult mcpconfig.SyncResult
var wg sync.WaitGroup

wg.Add(2)
go func() {
defer wg.Done()
updateResult = autoupdate.CheckWithResult()
}()
go func() {
defer wg.Done()
syncResult = mcpconfig.Sync()
}()
wg.Wait()

// Build status parts (same logic as claude shim)
var statusParts []string

if updateResult.Updated {
statusParts = append(statusParts, fmt.Sprintf("updated to %s", updateResult.NewVersion))
} else if updateResult.NewVersionAvailable {
statusParts = append(statusParts, fmt.Sprintf("update available: %s", updateResult.NewVersion))
}

if syncResult.NoAgentKey {
statusParts = append(statusParts, "no agent key")
} else if syncResult.Success {
if syncResult.HookCount > 0 {
statusParts = append(statusParts, fmt.Sprintf("%d hooks", syncResult.HookCount))
}
if syncResult.SkillCount > 0 {
statusParts = append(statusParts, fmt.Sprintf("%d skills", syncResult.SkillCount))
}
if syncResult.ServerCount > 0 {
statusParts = append(statusParts, fmt.Sprintf("%d servers", syncResult.ServerCount))
}
if syncResult.FromCache {
statusParts = append(statusParts, "cached")
}
} else {
statusParts = append(statusParts, "sync failed")
os.Exit(1)
}

if len(statusParts) > 0 {
fmt.Fprintf(os.Stderr, "[zeude init] %s\n", fmt.Sprintf("%s", strings.Join(statusParts, ", ")))
} else {
fmt.Fprintf(os.Stderr, "[zeude init] ok\n")
}
}

func runDoctor() {
home, err := os.UserHomeDir()
if err != nil {
Expand All @@ -90,10 +153,14 @@ func runDoctor() {
}

// Try to find and exec the doctor binary
doctorPath := filepath.Join(home, ".zeude", "bin", "zeude-doctor")
doctorName := "zeude-doctor"
if runtime.GOOS == "windows" {
doctorName = "zeude-doctor.exe"
}
doctorPath := filepath.Join(home, ".zeude", "bin", doctorName)
if _, err := os.Stat(doctorPath); err == nil {
// Found zeude-doctor binary - exec it
err = syscall.Exec(doctorPath, []string{"zeude-doctor"}, os.Environ())
err = executil.Exec(doctorPath, []string{"zeude-doctor"}, os.Environ())
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to exec zeude-doctor: %v\n", err)
os.Exit(1)
Expand All @@ -111,7 +178,11 @@ func runDoctor() {
fmt.Printf("%s[OK]%s Zeude version: %s\n", colorGreen, colorReset, version)

// Check shim
shimPath := filepath.Join(home, ".zeude", "bin", "claude")
shimName := "claude"
if runtime.GOOS == "windows" {
shimName = "claude.exe"
}
shimPath := filepath.Join(home, ".zeude", "bin", shimName)
if _, err := os.Stat(shimPath); err == nil {
fmt.Printf("%s[OK]%s Shim installed: %s\n", colorGreen, colorReset, shimPath)
} else {
Expand Down Expand Up @@ -176,7 +247,7 @@ func runDoctor() {
hookCount++
mode := info.Mode()
// Check if executable (user execute bit)
if mode&0100 == 0 {
if runtime.GOOS != "windows" && mode&0100 == 0 {
fmt.Printf("%s[FAIL]%s %s/%s: not executable (chmod +x needed)\n", colorRed, colorReset, eventDir.Name(), hookFile.Name())
hookIssues++
} else {
Expand All @@ -187,7 +258,17 @@ func runDoctor() {
if hookCount == 0 {
fmt.Printf("%s[INFO]%s No hooks installed\n", colorGray, colorReset)
} else if hookIssues > 0 {
fmt.Printf("\n%s[WARN]%s %d hook(s) have issues. Run: chmod +x ~/.claude/hooks/*/*\n", colorYellow, colorReset, hookIssues)
if runtime.GOOS == "windows" {
fmt.Printf("\n%s[WARN]%s %d hook(s) have issues. Try re-running: zeude sync\n", colorYellow, colorReset, hookIssues)
} else {
fmt.Printf("\n%s[WARN]%s %d hook(s) have issues. Run: chmod +x ~/.claude/hooks/*/*\n", colorYellow, colorReset, hookIssues)
}
}
// On Windows, check if bash is available for hook execution
if runtime.GOOS == "windows" {
if _, err := exec.LookPath("bash"); err != nil {
fmt.Printf("%s[WARN]%s Git Bash not found. Hooks will not execute. Install Git for Windows or set CLAUDE_CODE_GIT_BASH_PATH.\n", colorYellow, colorReset)
}
}
}
}
Expand All @@ -208,7 +289,7 @@ func ForceUpdate() error {
execPath, err := os.Executable()
if err == nil {
execPath, _ = filepath.EvalSymlinks(execPath)
syscall.Exec(execPath, os.Args, os.Environ())
executil.Exec(execPath, os.Args, os.Environ())
}
}

Expand Down
Loading