From 57fcaabfefffba4c60b5fdd0bff3b36c0dd8794c Mon Sep 17 00:00:00 2001
From: ganisback <370036720@qq.com>
Date: Sat, 30 May 2026 09:31:18 +0800
Subject: [PATCH] feat(apps): add Antigravity AI app install and launch
integration
Wire Antigravity into the AI apps catalog with cross-platform install scripts,
OSS mirror sync, and csghub-lite provider config for CLI launches.
---
docs/agent-guidelines/ai-app-oss-mirror.md | 7 +
docs/guides/packaging.md | 6 +
internal/antigravityagent/config.go | 135 ++++++++
internal/antigravityagent/config_test.go | 75 +++++
internal/apps/manager.go | 26 ++
internal/apps/manager_test.go | 27 ++
internal/apps/scripts/antigravity-install.ps1 | 138 ++++++++
internal/apps/scripts/antigravity-install.sh | 312 ++++++++++++++++++
.../apps/scripts/antigravity-uninstall.ps1 | 33 ++
.../apps/scripts/antigravity-uninstall.sh | 33 ++
internal/cli/launch.go | 8 +-
internal/cli/launch_prepare.go | 19 ++
internal/cli/launch_test.go | 4 +-
internal/server/handlers_apps.go | 2 +-
internal/server/handlers_apps_open.go | 2 +-
internal/server/handlers_apps_open_test.go | 60 ++++
internal/server/handlers_apps_shell.go | 20 ++
internal/server/static/apps/antigravity.svg | 14 +
local/secrets.env.example | 2 +
scripts/sync-ai-app-oss.sh | 192 ++++++++++-
scripts/sync-claude-code-oss.sh | 38 ++-
web/public/apps/antigravity.svg | 14 +
web/src/data/aiApps.ts | 44 +++
web/src/i18n.ts | 6 +
web/src/pages/AIAppShell.tsx | 2 +-
web/src/pages/AIApps.tsx | 16 +-
26 files changed, 1218 insertions(+), 17 deletions(-)
create mode 100644 internal/antigravityagent/config.go
create mode 100644 internal/antigravityagent/config_test.go
create mode 100644 internal/apps/scripts/antigravity-install.ps1
create mode 100644 internal/apps/scripts/antigravity-install.sh
create mode 100644 internal/apps/scripts/antigravity-uninstall.ps1
create mode 100644 internal/apps/scripts/antigravity-uninstall.sh
create mode 100644 internal/server/static/apps/antigravity.svg
create mode 100644 web/public/apps/antigravity.svg
diff --git a/docs/agent-guidelines/ai-app-oss-mirror.md b/docs/agent-guidelines/ai-app-oss-mirror.md
index 0dbfa06..ec475eb 100644
--- a/docs/agent-guidelines/ai-app-oss-mirror.md
+++ b/docs/agent-guidelines/ai-app-oss-mirror.md
@@ -9,6 +9,7 @@ This document covers mirroring AI app releases to the StarHub OSS bucket.
| claude-code | Anthropic GCS | `scripts/sync-claude-code-oss.sh` |
| open-code | GitHub: anomalyco/opencode | `scripts/sync-ai-app-oss.sh --app open-code` |
| codex | GitHub: openai/codex | `scripts/sync-ai-app-oss.sh --app codex` |
+| antigravity | Google Antigravity platform manifests | `scripts/sync-ai-app-oss.sh --app antigravity` |
## Sync Workflow
@@ -25,6 +26,9 @@ This document covers mirroring AI app releases to the StarHub OSS bucket.
# Check GitHub release version (for open-code, codex)
gh release view --repo anomalyco/opencode --json tagName --jq '.tagName'
gh release view --repo openai/codex --json tagName --jq '.tagName'
+
+ # Check Antigravity CLI upstream version
+ curl -fsSL https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/darwin_arm64.json
```
2. **Sync apps individually**: Sync one app at a time to avoid timeout issues.
@@ -37,6 +41,7 @@ This document covers mirroring AI app releases to the StarHub OSS bucket.
./scripts/sync-ai-app-oss.sh --app claude-code
./scripts/sync-ai-app-oss.sh --app open-code
./scripts/sync-ai-app-oss.sh --app codex
+ ./scripts/sync-ai-app-oss.sh --app antigravity
```
3. **Skip if already synced**: If the mirrored `latest` matches the upstream
@@ -69,6 +74,8 @@ Each app follows this versioned layout:
linux-x64, linux-x64-musl, win32-arm64, win32-x64
- **codex**: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64,
win32-x64 (uses musl builds for Linux)
+- **antigravity**: darwin-arm64, darwin-x64, linux-arm64, linux-x64,
+ win32-arm64, win32-x64
## Safety Rules
diff --git a/docs/guides/packaging.md b/docs/guides/packaging.md
index 5e25977..5a657f4 100644
--- a/docs/guides/packaging.md
+++ b/docs/guides/packaging.md
@@ -77,6 +77,7 @@ make package
| claude-code | Anthropic GCS | `./scripts/sync-ai-app-oss.sh --app claude-code` |
| open-code | GitHub: anomalyco/opencode | `./scripts/sync-ai-app-oss.sh --app open-code` |
| codex | GitHub: openai/codex | `./scripts/sync-ai-app-oss.sh --app codex` |
+| antigravity | Google Antigravity platform manifests | `./scripts/sync-ai-app-oss.sh --app antigravity` |
### 同步工作流
@@ -90,6 +91,9 @@ make package
# GitHub releases (open-code, codex)
gh release view --repo anomalyco/opencode --json tagName --jq '.tagName'
gh release view --repo openai/codex --json tagName --jq '.tagName'
+
+ # Antigravity CLI
+ curl -fsSL https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/darwin_arm64.json
```
2. **逐个同步应用**:每个应用下载需要几分钟,建议单独同步以避免超时。
@@ -100,6 +104,7 @@ make package
./scripts/sync-ai-app-oss.sh --app claude-code
./scripts/sync-ai-app-oss.sh --app open-code
./scripts/sync-ai-app-oss.sh --app codex
+ ./scripts/sync-ai-app-oss.sh --app antigravity
```
3. **版本一致则跳过**:如果镜像的 `latest` 与上游版本一致,无需重新同步。
@@ -129,6 +134,7 @@ make package
- `CSGHUB_LITE_CLAUDE_DIST_BASE_URL`
- `CSGHUB_LITE_OPEN_CODE_DIST_BASE_URL`
- `CSGHUB_LITE_CODEX_DIST_BASE_URL`
+- `CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL`
## GitLab 补发
diff --git a/internal/antigravityagent/config.go b/internal/antigravityagent/config.go
new file mode 100644
index 0000000..90434fb
--- /dev/null
+++ b/internal/antigravityagent/config.go
@@ -0,0 +1,135 @@
+package antigravityagent
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/opencsgs/csghub-lite/pkg/api"
+)
+
+const (
+ ProviderID = "LiteLLM"
+ APIKeyEnv = "CSGHUB_LITE_API_KEY"
+)
+
+// SyncConfig writes Antigravity CLI settings so launches use csghub-lite as the
+// OpenAI-compatible model provider.
+func SyncConfig(serverURL, apiKey, selectedModelID string, models []api.ModelInfo) error {
+ modelIDs := modelIDsFromInfos(models, selectedModelID)
+ if len(modelIDs) == 0 {
+ return fmt.Errorf("syncing Antigravity config: no models available")
+ }
+ if strings.TrimSpace(selectedModelID) == "" {
+ selectedModelID = modelIDs[0]
+ }
+
+ baseURL := strings.TrimRight(serverURL, "/") + "/v1"
+ _ = removeLegacyConfigTOML()
+ return writeSettingsJSON(baseURL, strings.TrimSpace(apiKey), strings.TrimSpace(selectedModelID))
+}
+
+// EnvOverrides returns process environment variables that force Antigravity to
+// use the csghub-lite gateway instead of falling back to its own auth flow.
+func EnvOverrides(serverURL, apiKey, selectedModelID string) map[string]string {
+ baseURL := strings.TrimRight(serverURL, "/") + "/v1"
+ apiKey = strings.TrimSpace(apiKey)
+
+ return map[string]string{
+ APIKeyEnv: apiKey,
+ "OPENAI_API_KEY": apiKey,
+ "OPENAI_BASE_URL": baseURL,
+ }
+}
+
+func modelIDsFromInfos(models []api.ModelInfo, selectedModelID string) []string {
+ seen := make(map[string]struct{}, len(models)+1)
+ out := make([]string, 0, len(models)+1)
+ for _, item := range models {
+ modelID := strings.TrimSpace(item.Model)
+ if modelID == "" {
+ continue
+ }
+ if _, ok := seen[modelID]; ok {
+ continue
+ }
+ seen[modelID] = struct{}{}
+ out = append(out, modelID)
+ }
+ if selectedModelID = strings.TrimSpace(selectedModelID); selectedModelID != "" {
+ if _, ok := seen[selectedModelID]; !ok {
+ out = append([]string{selectedModelID}, out...)
+ }
+ }
+ return out
+}
+
+func removeLegacyConfigTOML() error {
+ configPath, err := ConfigPath()
+ if err != nil {
+ return err
+ }
+ if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+func writeSettingsJSON(baseURL, apiKey, selectedModelID string) error {
+ settingsPath, err := SettingsPath()
+ if err != nil {
+ return err
+ }
+ return syncJSONFile(settingsPath, func(doc map[string]interface{}) {
+ doc["model"] = selectedModelID
+ doc["customProviders"] = []map[string]interface{}{
+ {
+ "name": ProviderID,
+ "type": "openai",
+ "baseUrl": baseURL,
+ "apiKey": apiKey,
+ "modelId": selectedModelID,
+ },
+ }
+ delete(doc, "env")
+ })
+}
+
+// ConfigPath returns ~/.config/antigravity/config.toml.
+func ConfigPath() (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(home, ".config", "antigravity", "config.toml"), nil
+}
+
+// SettingsPath returns ~/.gemini/antigravity-cli/settings.json.
+func SettingsPath() (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(home, ".gemini", "antigravity-cli", "settings.json"), nil
+}
+
+func syncJSONFile(path string, mutate func(map[string]interface{})) error {
+ doc := map[string]interface{}{}
+ if data, err := os.ReadFile(path); err == nil && len(data) > 0 {
+ _ = json.Unmarshal(data, &doc)
+ }
+ mutate(doc)
+
+ data, err := json.MarshalIndent(doc, "", " ")
+ if err != nil {
+ return err
+ }
+ data = append(data, '\n')
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ return os.WriteFile(path, data, 0o644)
+}
diff --git a/internal/antigravityagent/config_test.go b/internal/antigravityagent/config_test.go
new file mode 100644
index 0000000..68d3950
--- /dev/null
+++ b/internal/antigravityagent/config_test.go
@@ -0,0 +1,75 @@
+package antigravityagent
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/opencsgs/csghub-lite/pkg/api"
+)
+
+func TestSyncConfigWritesAntigravityProviderFiles(t *testing.T) {
+ home := t.TempDir()
+ t.Setenv("HOME", home)
+
+ models := []api.ModelInfo{
+ {Model: "Qwen/Qwen3.5-2B"},
+ {Model: "minimax-m2.5"},
+ }
+ legacyConfigPath := filepath.Join(home, ".config", "antigravity", "config.toml")
+ if err := os.MkdirAll(filepath.Dir(legacyConfigPath), 0o755); err != nil {
+ t.Fatalf("mkdir legacy config dir: %v", err)
+ }
+ if err := os.WriteFile(legacyConfigPath, []byte("[[custom_models]]\n"), 0o644); err != nil {
+ t.Fatalf("write legacy config: %v", err)
+ }
+
+ if err := SyncConfig("http://127.0.0.1:11435", "csghub-lite", "Qwen/Qwen3.5-2B", models); err != nil {
+ t.Fatalf("SyncConfig returned error: %v", err)
+ }
+ if _, err := os.Stat(legacyConfigPath); !os.IsNotExist(err) {
+ t.Fatalf("legacy config.toml should be removed, stat err = %v", err)
+ }
+
+ settingsText, err := os.ReadFile(filepath.Join(home, ".gemini", "antigravity-cli", "settings.json"))
+ if err != nil {
+ t.Fatalf("read settings.json: %v", err)
+ }
+ var settings struct {
+ Model string `json:"model"`
+ CustomProviders []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ BaseURL string `json:"baseUrl"`
+ APIKey string `json:"apiKey"`
+ ModelID string `json:"modelId"`
+ } `json:"customProviders"`
+ }
+ if err := json.Unmarshal(settingsText, &settings); err != nil {
+ t.Fatalf("decode settings.json: %v", err)
+ }
+ if settings.Model != "Qwen/Qwen3.5-2B" {
+ t.Fatalf("model = %q, want selected model", settings.Model)
+ }
+ if len(settings.CustomProviders) != 1 {
+ t.Fatalf("customProviders = %#v, want one provider", settings.CustomProviders)
+ }
+ provider := settings.CustomProviders[0]
+ for got, want := range map[string]string{
+ provider.Name: ProviderID,
+ provider.Type: "openai",
+ provider.BaseURL: "http://127.0.0.1:11435/v1",
+ provider.APIKey: "csghub-lite",
+ provider.ModelID: "Qwen/Qwen3.5-2B",
+ } {
+ if got != want {
+ t.Fatalf("custom provider value = %q, want %q; provider=%#v", got, want, provider)
+ }
+ }
+
+ if strings.Contains(string(settingsText), `"env"`) {
+ t.Fatalf("settings.json should not include legacy env block:\n%s", settingsText)
+ }
+}
diff --git a/internal/apps/manager.go b/internal/apps/manager.go
index 0ef18b7..4549aad 100644
--- a/internal/apps/manager.go
+++ b/internal/apps/manager.go
@@ -278,6 +278,32 @@ func appSpecs() []appSpec {
embeddedPath: "scripts/codex-uninstall.ps1",
},
},
+ {
+ id: "antigravity",
+ binaryName: "agy",
+ installMode: "script",
+ progressMode: progressModePercent,
+ supported: true,
+ versionArgs: []string{"--version"},
+ latest: &latestVersionSource{
+ baseURL: "https://opencsg-public-resource.oss-cn-beijing.aliyuncs.com/antigravity-releases",
+ envVar: "CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL",
+ },
+ unix: &scriptSource{
+ mirrorURL: mirrorBaseURL + "/antigravity/install.sh",
+ embeddedPath: "scripts/antigravity-install.sh",
+ },
+ windows: &scriptSource{
+ mirrorURL: mirrorBaseURL + "/antigravity/install.ps1",
+ embeddedPath: "scripts/antigravity-install.ps1",
+ },
+ uninstallUnix: &scriptSource{
+ embeddedPath: "scripts/antigravity-uninstall.sh",
+ },
+ uninstallWin: &scriptSource{
+ embeddedPath: "scripts/antigravity-uninstall.ps1",
+ },
+ },
{
id: "pi",
binaryName: "pi",
diff --git a/internal/apps/manager_test.go b/internal/apps/manager_test.go
index f77e53f..19dfb06 100644
--- a/internal/apps/manager_test.go
+++ b/internal/apps/manager_test.go
@@ -72,6 +72,33 @@ func TestClaudeInstallerMirrorURLsUseCurrentRepoScripts(t *testing.T) {
}
}
+func TestAntigravityAppSpecUsesMirroredInstallerAndAgyBinary(t *testing.T) {
+ var antigravity appSpec
+ found := false
+ for _, spec := range appSpecs() {
+ if spec.id == "antigravity" {
+ antigravity = spec
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatal("antigravity spec not found")
+ }
+ if antigravity.binaryName != "agy" {
+ t.Fatalf("antigravity binaryName = %q, want agy", antigravity.binaryName)
+ }
+ if antigravity.latest == nil || antigravity.latest.envVar != "CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL" {
+ t.Fatalf("antigravity latest source = %#v, want mirror env override", antigravity.latest)
+ }
+ if antigravity.unix == nil || antigravity.unix.mirrorURL != mirrorBaseURL+"/antigravity/install.sh" {
+ t.Fatalf("antigravity unix installer = %#v, want mirrored install.sh", antigravity.unix)
+ }
+ if antigravity.windows == nil || antigravity.windows.mirrorURL != mirrorBaseURL+"/antigravity/install.ps1" {
+ t.Fatalf("antigravity windows installer = %#v, want mirrored install.ps1", antigravity.windows)
+ }
+}
+
func TestDetectInstalledBinaryPathFallsBackToCommonDirs(t *testing.T) {
homeDir := setTempHome(t)
t.Setenv("PATH", "")
diff --git a/internal/apps/scripts/antigravity-install.ps1 b/internal/apps/scripts/antigravity-install.ps1
new file mode 100644
index 0000000..28d44ad
--- /dev/null
+++ b/internal/apps/scripts/antigravity-install.ps1
@@ -0,0 +1,138 @@
+param(
+ [string]$Target = "latest"
+)
+
+$ErrorActionPreference = "Stop"
+$DefaultDistBaseUrl = "https://opencsg-public-resource.oss-cn-beijing.aliyuncs.com/antigravity-releases"
+$DistBaseUrl = if ($env:CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL) { $env:CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL } else { $DefaultDistBaseUrl }
+
+function Emit-Progress([int]$Percent, [string]$Phase) {
+ Write-Output "CSGHUB_PROGRESS|$Percent|$Phase"
+}
+
+function Trim-TrailingSlash([string]$Value) {
+ if ([string]::IsNullOrWhiteSpace($Value)) {
+ return $Value
+ }
+ return $Value.TrimEnd('/')
+}
+
+function Normalize-RequestedVersion([string]$Value) {
+ if ([string]::IsNullOrWhiteSpace($Value) -or $Value -eq "latest") {
+ return "latest"
+ }
+ return $Value.TrimStart('v')
+}
+
+function Ensure-PathContains([string]$Dir) {
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
+ $parts = @()
+ if ($userPath) { $parts = $userPath.Split(';') }
+ if ($parts -notcontains $Dir) {
+ $newPath = if ($userPath) { "$Dir;$userPath" } else { $Dir }
+ [Environment]::SetEnvironmentVariable("Path", $newPath, "User")
+ }
+ if ($env:Path -notlike "*$Dir*") {
+ $env:Path = "$Dir;$env:Path"
+ }
+}
+
+if (-not [Environment]::Is64BitProcess) {
+ throw "Antigravity does not support 32-bit Windows."
+}
+
+$distBaseUrl = Trim-TrailingSlash $DistBaseUrl
+$requestedVersion = Normalize-RequestedVersion $Target
+$workDir = Join-Path $env:TEMP ("antigravity-install-" + [guid]::NewGuid().ToString("N"))
+$downloadDir = Join-Path $workDir "downloads"
+
+try {
+ Emit-Progress 10 "detecting_platform"
+ if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
+ $platform = "win32-arm64"
+ $upstreamPlatform = "windows_arm64"
+ } else {
+ $platform = "win32-x64"
+ $upstreamPlatform = "windows_amd64"
+ }
+
+ New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null
+
+ Emit-Progress 25 "resolving_latest"
+ $version = $null
+ $checksum = $null
+ $assetName = $null
+ $archiveFormat = $null
+ $downloadUrl = $null
+ $downloadSource = $null
+
+ try {
+ $version = if ($requestedVersion -eq "latest") {
+ (Invoke-RestMethod -Uri "$distBaseUrl/latest" -ErrorAction Stop).ToString().Trim()
+ } else {
+ $requestedVersion
+ }
+ if (-not [string]::IsNullOrWhiteSpace($version)) {
+ $manifest = Invoke-RestMethod -Uri "$distBaseUrl/$version/manifest.json" -ErrorAction Stop
+ $platformMeta = $manifest.platforms.$platform
+ if ($platformMeta) {
+ $checksum = $platformMeta.checksum_sha512
+ $assetName = $platformMeta.asset
+ $archiveFormat = $platformMeta.archive_format
+ if ($checksum -and $assetName -and $archiveFormat) {
+ $downloadUrl = "$distBaseUrl/$version/$platform/$assetName"
+ $downloadSource = $distBaseUrl
+ }
+ }
+ }
+ } catch {
+ $downloadUrl = $null
+ }
+
+ if (-not $downloadUrl) {
+ if ($requestedVersion -ne "latest") {
+ throw "mirrored Antigravity version $requestedVersion is unavailable"
+ }
+ Write-Output "INFO: Antigravity mirror is not ready; falling back to the official Google CLI release manifest"
+ $manifest = Invoke-RestMethod -Uri "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/$upstreamPlatform.json" -ErrorAction Stop
+ $version = $manifest.version
+ $downloadUrl = $manifest.url
+ $checksum = $manifest.sha512
+ $assetName = Split-Path ([uri]$downloadUrl).AbsolutePath -Leaf
+ $archiveFormat = "raw"
+ $downloadSource = "official"
+ }
+
+ if ([string]::IsNullOrWhiteSpace($version) -or -not $downloadUrl -or -not $checksum -or -not $assetName -or -not $archiveFormat) {
+ throw "failed to resolve Antigravity version"
+ }
+
+ $archivePath = Join-Path $downloadDir $assetName
+ Emit-Progress 55 "downloading_archive"
+ Write-Output "INFO: downloading Antigravity $version for $platform from $downloadSource"
+ Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -ErrorAction Stop
+
+ Emit-Progress 75 "verifying_checksum"
+ $actualChecksum = (Get-FileHash -Path $archivePath -Algorithm SHA512).Hash.ToLower()
+ if ($actualChecksum -ne $checksum) {
+ throw "checksum verification failed"
+ }
+
+ Emit-Progress 90 "installing_runtime"
+ $launcherDir = Join-Path $env:USERPROFILE ".local\bin"
+ $launcherPath = Join-Path $launcherDir "agy.exe"
+ New-Item -ItemType Directory -Force -Path $launcherDir | Out-Null
+ Copy-Item -Path $archivePath -Destination $launcherPath -Force
+ Unblock-File -Path $launcherPath -ErrorAction SilentlyContinue
+ Ensure-PathContains -Dir $launcherDir
+
+ try { & $launcherPath install } catch {}
+
+ Emit-Progress 100 "complete"
+ try { & $launcherPath --version } catch {}
+ Write-Output "INFO: Antigravity installation complete"
+} finally {
+ if (Test-Path $workDir) {
+ Remove-Item -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue
+ }
+}
diff --git a/internal/apps/scripts/antigravity-install.sh b/internal/apps/scripts/antigravity-install.sh
new file mode 100644
index 0000000..f249fce
--- /dev/null
+++ b/internal/apps/scripts/antigravity-install.sh
@@ -0,0 +1,312 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+TARGET="${1:-latest}"
+DEFAULT_DIST_BASE_URL="https://opencsg-public-resource.oss-cn-beijing.aliyuncs.com/antigravity-releases"
+DIST_BASE_URL="${CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL:-$DEFAULT_DIST_BASE_URL}"
+WORKDIR=""
+DOWNLOADER=""
+
+emit_progress() {
+ printf 'CSGHUB_PROGRESS|%s|%s\n' "$1" "$2"
+}
+
+log() {
+ printf '%s\n' "$*"
+}
+
+cleanup() {
+ if [[ -n "${WORKDIR}" && -d "${WORKDIR}" ]]; then
+ rm -rf "${WORKDIR}"
+ fi
+}
+
+trap cleanup EXIT
+
+trim_trailing_slash() {
+ local value="$1"
+ while [[ "$value" == */ ]]; do
+ value="${value%/}"
+ done
+ printf '%s\n' "$value"
+}
+
+select_downloader() {
+ if command -v curl >/dev/null 2>&1; then
+ DOWNLOADER="curl"
+ return 0
+ fi
+ if command -v wget >/dev/null 2>&1; then
+ DOWNLOADER="wget"
+ return 0
+ fi
+ log "ERROR: either curl or wget is required"
+ exit 1
+}
+
+download_text() {
+ local url="$1"
+ if [[ "$DOWNLOADER" == "curl" ]]; then
+ curl --connect-timeout 15 --max-time 60 --retry 3 --retry-delay 2 -fsSL "$url"
+ else
+ wget --tries=3 --timeout=20 -q -O - "$url"
+ fi
+}
+
+download_file() {
+ local url="$1"
+ local output="$2"
+ if [[ "$DOWNLOADER" == "curl" ]]; then
+ curl --connect-timeout 15 --max-time 1800 --retry 3 --retry-delay 2 -fsSL -o "$output" "$url"
+ else
+ wget --tries=3 --timeout=30 -O "$output" "$url"
+ fi
+}
+
+get_platform_entry_from_manifest() {
+ local json="$1"
+ local platform="$2"
+ json="$(printf '%s' "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g')"
+ if [[ $json =~ \"$platform\"[[:space:]]*:[[:space:]]*\{([^}]*)\} ]]; then
+ printf '%s\n' "${BASH_REMATCH[1]}"
+ return 0
+ fi
+ return 1
+}
+
+get_platform_string_field() {
+ local entry="$1"
+ local field="$2"
+ if [[ $entry =~ \"$field\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
+ printf '%s\n' "${BASH_REMATCH[1]}"
+ return 0
+ fi
+ return 1
+}
+
+parse_json_key() {
+ local payload="$1"
+ local key="$2"
+ printf '%s\n' "$payload" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
+}
+
+shell_profile_file() {
+ local home_dir="${HOME:-}"
+ if [[ -z "$home_dir" ]]; then
+ return 1
+ fi
+ case "$(basename "${SHELL:-}")" in
+ zsh) printf '%s\n' "${home_dir}/.zprofile" ;;
+ bash) printf '%s\n' "${home_dir}/.bash_profile" ;;
+ *) printf '%s\n' "${home_dir}/.profile" ;;
+ esac
+}
+
+ensure_local_bin_on_path() {
+ local bin_dir="${HOME}/.local/bin"
+ local profile=""
+ local line='case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; esac'
+
+ export PATH="${bin_dir}:${PATH}"
+
+ profile="$(shell_profile_file || true)"
+ if [[ -z "$profile" ]]; then
+ return 0
+ fi
+ mkdir -p "$(dirname "$profile")"
+ [[ -f "$profile" ]] || : > "$profile"
+ if ! grep -F "$line" "$profile" >/dev/null 2>&1; then
+ printf '\n%s\n' "$line" >> "$profile"
+ fi
+}
+
+sha512_file() {
+ local path="$1"
+ if command -v sha512sum >/dev/null 2>&1; then
+ sha512sum "$path" | awk '{print $1}'
+ return 0
+ fi
+ if command -v shasum >/dev/null 2>&1; then
+ shasum -a 512 "$path" | awk '{print $1}'
+ return 0
+ fi
+ log "ERROR: sha512sum or shasum is required"
+ exit 1
+}
+
+resolve_requested_version() {
+ local requested="${TARGET:-latest}"
+ requested="${requested#v}"
+ if [[ -z "$requested" || "$requested" == "latest" ]]; then
+ download_text "$(trim_trailing_slash "$DIST_BASE_URL")/latest" | tr -d '[:space:]'
+ return 0
+ fi
+ printf '%s\n' "$requested"
+}
+
+detect_platform() {
+ local os=""
+ local arch=""
+
+ case "$(uname -s)" in
+ Darwin) os="darwin" ;;
+ Linux) os="linux" ;;
+ MINGW*|MSYS*|CYGWIN*)
+ log "ERROR: use the PowerShell installer on Windows"
+ exit 1
+ ;;
+ *)
+ log "ERROR: unsupported operating system $(uname -s)"
+ exit 1
+ ;;
+ esac
+
+ case "$(uname -m)" in
+ x86_64|amd64) arch="x64" ;;
+ arm64|aarch64) arch="arm64" ;;
+ *)
+ log "ERROR: unsupported architecture $(uname -m)"
+ exit 1
+ ;;
+ esac
+
+ printf '%s-%s\n' "$os" "$arch"
+}
+
+upstream_platform_for() {
+ case "$1" in
+ darwin-arm64) printf '%s\n' "darwin_arm64" ;;
+ darwin-x64) printf '%s\n' "darwin_amd64" ;;
+ linux-arm64) printf '%s\n' "linux_arm64" ;;
+ linux-x64) printf '%s\n' "linux_amd64" ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+install_antigravity() {
+ local platform=""
+ local upstream_platform=""
+ local version=""
+ local dist_base_url=""
+ local manifest_json=""
+ local platform_entry=""
+ local checksum=""
+ local asset_name=""
+ local archive_format=""
+ local binary_name=""
+ local download_url=""
+ local download_source=""
+ local archive_path=""
+ local extract_dir=""
+ local extracted_binary=""
+ local launcher_dir="${HOME}/.local/bin"
+ local launcher_path="${launcher_dir}/agy"
+ local actual=""
+
+ select_downloader
+
+ emit_progress 10 detecting_platform
+ platform="$(detect_platform)"
+ upstream_platform="$(upstream_platform_for "$platform" || true)"
+
+ WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/antigravity-install.XXXXXX")"
+ dist_base_url="$(trim_trailing_slash "$DIST_BASE_URL")"
+
+ emit_progress 25 resolving_latest
+ version="$(resolve_requested_version || true)"
+ version="$(printf '%s' "$version" | tr -d '[:space:]')"
+
+ if [[ -n "$version" ]]; then
+ manifest_json="$(download_text "$dist_base_url/$version/manifest.json" || true)"
+ platform_entry="$(get_platform_entry_from_manifest "$manifest_json" "$platform" || true)"
+ checksum="$(get_platform_string_field "$platform_entry" "checksum_sha512" || true)"
+ asset_name="$(get_platform_string_field "$platform_entry" "asset" || true)"
+ archive_format="$(get_platform_string_field "$platform_entry" "archive_format" || true)"
+ binary_name="$(get_platform_string_field "$platform_entry" "binary" || true)"
+ if [[ -n "$checksum" && -n "$asset_name" && -n "$archive_format" && -n "$binary_name" ]]; then
+ download_url="$dist_base_url/$version/$platform/$asset_name"
+ download_source="$dist_base_url"
+ fi
+ fi
+
+ if [[ -z "$download_url" ]]; then
+ if [[ "${TARGET:-latest}" != "latest" && -n "${TARGET:-}" ]]; then
+ log "ERROR: mirrored Antigravity version ${TARGET} is unavailable"
+ exit 1
+ fi
+ if [[ -z "$upstream_platform" ]]; then
+ log "ERROR: unsupported Antigravity platform ${platform}"
+ exit 1
+ fi
+ log "INFO: Antigravity mirror is not ready; falling back to the official Google CLI release manifest"
+ manifest_json="$(download_text "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/${upstream_platform}.json" || true)"
+ version="$(parse_json_key "$manifest_json" "version")"
+ download_url="$(parse_json_key "$manifest_json" "url")"
+ checksum="$(parse_json_key "$manifest_json" "sha512")"
+ asset_name="${download_url%%\?*}"
+ asset_name="${asset_name##*/}"
+ if [[ "$download_url" == *.tar.gz* ]]; then
+ archive_format="tar.gz"
+ binary_name="antigravity"
+ else
+ archive_format="raw"
+ binary_name="agy"
+ fi
+ download_source="official"
+ fi
+
+ if [[ -z "$version" || -z "$download_url" || -z "$checksum" || -z "$asset_name" || -z "$archive_format" || -z "$binary_name" ]]; then
+ log "ERROR: failed to resolve Antigravity version"
+ exit 1
+ fi
+
+ archive_path="${WORKDIR}/${asset_name}"
+ extract_dir="${WORKDIR}/extract"
+
+ emit_progress 55 downloading_archive
+ log "INFO: downloading Antigravity ${version} for ${platform} from ${download_source}"
+ download_file "$download_url" "$archive_path"
+
+ emit_progress 75 verifying_checksum
+ actual="$(sha512_file "$archive_path")"
+ if [[ "$actual" != "$checksum" ]]; then
+ log "ERROR: checksum verification failed"
+ exit 1
+ fi
+
+ emit_progress 90 installing_runtime
+ mkdir -p "$launcher_dir" "$extract_dir"
+ case "$archive_format" in
+ tar.gz)
+ tar -xzf "$archive_path" -C "$extract_dir" "$binary_name"
+ extracted_binary="${extract_dir}/${binary_name}"
+ ;;
+ raw)
+ extracted_binary="$archive_path"
+ ;;
+ *)
+ log "ERROR: unsupported archive format ${archive_format}"
+ exit 1
+ ;;
+ esac
+ [[ -f "$extracted_binary" ]] || {
+ log "ERROR: Antigravity binary not found after extraction"
+ exit 1
+ }
+ cp "$extracted_binary" "$launcher_path"
+ chmod +x "$launcher_path"
+ if [[ "$(uname -s)" == "Darwin" ]]; then
+ xattr -d com.apple.quarantine "$launcher_path" 2>/dev/null || true
+ fi
+ ensure_local_bin_on_path
+
+ "$launcher_path" install || true
+
+ emit_progress 100 complete
+ "$launcher_path" --version || true
+ log "INFO: Antigravity installation complete"
+}
+
+install_antigravity
diff --git a/internal/apps/scripts/antigravity-uninstall.ps1 b/internal/apps/scripts/antigravity-uninstall.ps1
new file mode 100644
index 0000000..d5ede59
--- /dev/null
+++ b/internal/apps/scripts/antigravity-uninstall.ps1
@@ -0,0 +1,33 @@
+$ErrorActionPreference = "Stop"
+
+function Emit-Progress([int]$Percent, [string]$Phase) {
+ Write-Output "CSGHUB_PROGRESS|$Percent|$Phase"
+}
+
+Emit-Progress 5 "preflight"
+
+$launchers = @(
+ (Join-Path $env:USERPROFILE ".local\bin\agy.exe"),
+ (Join-Path $env:USERPROFILE "bin\agy.exe")
+)
+
+Emit-Progress 55 "removing_runtime"
+foreach ($launcher in $launchers) {
+ if (Test-Path $launcher) {
+ Remove-Item -Path $launcher -Force
+ }
+}
+
+$stagingDir = Join-Path $env:LOCALAPPDATA "antigravity\staging"
+if (Test-Path $stagingDir) {
+ Remove-Item -Path $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+Emit-Progress 80 "verifying_uninstall"
+if (Get-Command agy -ErrorAction SilentlyContinue) {
+ Write-Output "ERROR: Antigravity binary is still available at $((Get-Command agy).Source)"
+ exit 1
+}
+
+Emit-Progress 100 "complete"
+Write-Output "INFO: Antigravity uninstallation complete"
diff --git a/internal/apps/scripts/antigravity-uninstall.sh b/internal/apps/scripts/antigravity-uninstall.sh
new file mode 100644
index 0000000..64a0a08
--- /dev/null
+++ b/internal/apps/scripts/antigravity-uninstall.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+emit_progress() {
+ printf 'CSGHUB_PROGRESS|%s|%s\n' "$1" "$2"
+}
+
+log() {
+ printf '%s\n' "$*"
+}
+
+emit_progress 5 preflight
+
+launchers=(
+ "${HOME}/.local/bin/agy"
+ "${HOME}/bin/agy"
+)
+
+emit_progress 55 removing_runtime
+for launcher in "${launchers[@]}"; do
+ rm -f "${launcher}"
+done
+rm -rf "${HOME}/.cache/antigravity/staging"
+hash -r 2>/dev/null || true
+
+emit_progress 80 verifying_uninstall
+if command -v agy >/dev/null 2>&1; then
+ log "ERROR: Antigravity binary is still available at $(command -v agy)"
+ exit 1
+fi
+
+emit_progress 100 complete
+log "INFO: Antigravity uninstallation complete"
diff --git a/internal/cli/launch.go b/internal/cli/launch.go
index 5e5f6e9..0404bc4 100644
--- a/internal/cli/launch.go
+++ b/internal/cli/launch.go
@@ -33,7 +33,7 @@ type launchOptions struct {
Gateway string
}
-const launchSupportedApps = "claude-code, open-code, codex, pi, openclaw, csgclaw, dify, anythingllm"
+const launchSupportedApps = "claude-code, open-code, codex, antigravity, pi, openclaw, csgclaw, dify, anythingllm"
const claudeDangerouslySkipPermissionsFlag = "dangerously-skip-permissions"
func newLaunchCmd() *cobra.Command {
@@ -251,6 +251,12 @@ func resolveLaunchTarget(name string) (launchTarget, error) {
DisplayName: "Pi",
Binaries: []string{"pi"},
}, nil
+ case "antigravity", "agy":
+ return launchTarget{
+ AppID: "antigravity",
+ DisplayName: "Antigravity",
+ Binaries: []string{"agy"},
+ }, nil
case "openclaw":
return launchTarget{
AppID: "openclaw",
diff --git a/internal/cli/launch_prepare.go b/internal/cli/launch_prepare.go
index 73d2506..fa3103d 100644
--- a/internal/cli/launch_prepare.go
+++ b/internal/cli/launch_prepare.go
@@ -13,6 +13,7 @@ import (
"strings"
"time"
+ "github.com/opencsgs/csghub-lite/internal/antigravityagent"
"github.com/opencsgs/csghub-lite/internal/claudeagent"
"github.com/opencsgs/csghub-lite/internal/codexagent"
"github.com/opencsgs/csghub-lite/internal/config"
@@ -225,6 +226,8 @@ func prepareLaunchExecution(target launchTarget, serverURL, modelID string, user
return prepareCodexLaunch(target, serverURL, modelID, userArgs)
case "pi":
return preparePiLaunch(target, serverURL, modelID, userArgs)
+ case "antigravity":
+ return prepareAntigravityLaunch(target, serverURL, modelID, userArgs)
case "openclaw":
return prepareOpenClawLaunch(target, serverURL, modelID, userArgs)
case "csgclaw":
@@ -293,6 +296,22 @@ func prepareCodexLaunch(target launchTarget, serverURL, modelID string, userArgs
return preparedLaunch{Binary: binary, Args: args, Env: env}, nil
}
+func prepareAntigravityLaunch(target launchTarget, serverURL, modelID string, userArgs []string) (preparedLaunch, error) {
+ binary, err := resolveLaunchBinary(target.Binaries)
+ if err != nil {
+ return preparedLaunch{}, fmt.Errorf("%s is installed, but the launch command was not found on PATH", target.DisplayName)
+ }
+ models, err := getLaunchModels(serverURL)
+ if err != nil {
+ return preparedLaunch{}, err
+ }
+ if err := antigravityagent.SyncConfig(serverURL, openClawProviderAPIKey(config.Get().Token), modelID, models); err != nil {
+ return preparedLaunch{}, err
+ }
+ env := envWithOverrides(antigravityagent.EnvOverrides(serverURL, openClawProviderAPIKey(config.Get().Token), modelID))
+ return preparedLaunch{Binary: binary, Args: append([]string{}, userArgs...), Env: env}, nil
+}
+
func preparePiLaunch(target launchTarget, serverURL, modelID string, userArgs []string) (preparedLaunch, error) {
binary, err := resolveLaunchBinary(target.Binaries)
if err != nil {
diff --git a/internal/cli/launch_test.go b/internal/cli/launch_test.go
index 6082b16..fa2e083 100644
--- a/internal/cli/launch_test.go
+++ b/internal/cli/launch_test.go
@@ -16,6 +16,8 @@ func TestResolveLaunchTarget(t *testing.T) {
{input: "open-code", want: "open-code"},
{input: "opencode", want: "open-code"},
{input: "codex", want: "codex"},
+ {input: "antigravity", want: "antigravity"},
+ {input: "agy", want: "antigravity"},
{input: "pi", want: "pi"},
{input: "openclaw", want: "openclaw"},
{input: "csgclaw", want: "csgclaw"},
@@ -60,7 +62,7 @@ func TestLaunchCmdHelpListsSupportedAppsAndExamples(t *testing.T) {
output := buf.String()
for _, want := range []string{
"Supported apps:",
- "claude-code, open-code, codex, pi, openclaw, csgclaw, dify, anythingllm",
+ "claude-code, open-code, codex, antigravity, pi, openclaw, csgclaw, dify, anythingllm",
"csghub-lite launch pi",
"csghub-lite launch csgclaw",
"csghub-lite launch open-code -- --help",
diff --git a/internal/server/handlers_apps.go b/internal/server/handlers_apps.go
index d04f5c9..3c71f61 100644
--- a/internal/server/handlers_apps.go
+++ b/internal/server/handlers_apps.go
@@ -256,7 +256,7 @@ func (s *Server) enrichAIApp(ctx context.Context, info *api.AIAppInfo) {
)
switch info.ID {
- case "claude-code", "open-code", "codex", "pi":
+ case "claude-code", "open-code", "codex", "pi", "antigravity":
modelID, _, err = s.resolveAIAppShellLaunchModels(ctx, info.ID, "", "")
case "openclaw", "csgclaw":
preferred := s.preferredAIAppModel(info.ID)
diff --git a/internal/server/handlers_apps_open.go b/internal/server/handlers_apps_open.go
index 2aff4c8..92ea2e4 100644
--- a/internal/server/handlers_apps_open.go
+++ b/internal/server/handlers_apps_open.go
@@ -60,7 +60,7 @@ func (s *Server) openAIAppURL(ctx context.Context, appID, modelID, modelSource,
return "", err
}
return rewriteLoopbackURLHost(url, publicBaseURL), nil
- case "claude-code", "open-code", "codex", "pi":
+ case "claude-code", "open-code", "codex", "pi", "antigravity":
return s.openAIAppShellURL(ctx, appID, modelID, modelSource, workDir, publicBaseURL)
default:
return "", fmt.Errorf("%s does not provide a direct chat entry yet", appID)
diff --git a/internal/server/handlers_apps_open_test.go b/internal/server/handlers_apps_open_test.go
index 3956ea5..94b566c 100644
--- a/internal/server/handlers_apps_open_test.go
+++ b/internal/server/handlers_apps_open_test.go
@@ -1257,6 +1257,66 @@ func TestPrepareAIAppShellLaunchUsesPiProviderConfig(t *testing.T) {
}
}
+func TestPrepareAIAppShellLaunchUsesAntigravityProviderConfig(t *testing.T) {
+ home := t.TempDir()
+ t.Setenv("HOME", home)
+
+ binDir := t.TempDir()
+ commandPath := filepath.Join(binDir, "agy")
+ content := "#!/bin/sh\nexit 0\n"
+ if runtime.GOOS == "windows" {
+ commandPath = filepath.Join(binDir, "agy.cmd")
+ content = "@echo off\r\nexit /b 0\r\n"
+ }
+ if err := os.WriteFile(commandPath, []byte(content), 0o755); err != nil {
+ t.Fatalf("write fake binary: %v", err)
+ }
+ t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ s := New(&config.Config{ListenAddr: ":11435"}, "test")
+ workDir := t.TempDir()
+ prepared, err := s.prepareAIAppShellLaunch(aiAppOpenTarget{
+ AppID: "antigravity",
+ DisplayName: "Antigravity",
+ Binaries: []string{"agy"},
+ }, "Qwen/Qwen3.5-2B", []string{"Qwen/Qwen3.5-2B", "minimax-m2.5"}, workDir)
+ if err != nil {
+ t.Fatalf("prepareAIAppShellLaunch returned error: %v", err)
+ }
+ if prepared.Binary == "" {
+ t.Fatalf("expected launch binary, got empty")
+ }
+ if got := envValue(prepared.Env, "CSGHUB_LITE_API_KEY"); got != "csghub-lite" {
+ t.Fatalf("CSGHUB_LITE_API_KEY = %q, want csghub-lite", got)
+ }
+ if got := envValue(prepared.Env, "OPENAI_BASE_URL"); got != "http://127.0.0.1:11435/v1" {
+ t.Fatalf("OPENAI_BASE_URL = %q, want local v1 URL", got)
+ }
+ if envHasKey(prepared.Env, "NO_COLOR") {
+ t.Fatalf("NO_COLOR should be removed from Antigravity web shell environment: %#v", prepared.Env)
+ }
+
+ settingsText, err := os.ReadFile(filepath.Join(home, ".gemini", "antigravity-cli", "settings.json"))
+ if err != nil {
+ t.Fatalf("read Antigravity settings: %v", err)
+ }
+ if !strings.Contains(string(settingsText), `"model": "Qwen/Qwen3.5-2B"`) {
+ t.Fatalf("settings.json = %q, want selected model", settingsText)
+ }
+ for _, want := range []string{
+ `"customProviders": [`,
+ `"name": "LiteLLM"`,
+ `"type": "openai"`,
+ `"baseUrl": "http://127.0.0.1:11435/v1"`,
+ `"apiKey": "csghub-lite"`,
+ `"modelId": "Qwen/Qwen3.5-2B"`,
+ } {
+ if !strings.Contains(string(settingsText), want) {
+ t.Fatalf("settings.json missing %q:\n%s", want, settingsText)
+ }
+ }
+}
+
func TestWriteOpenCodeWebLaunchConfigIncludesAllModels(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
diff --git a/internal/server/handlers_apps_shell.go b/internal/server/handlers_apps_shell.go
index 296f9ba..ee52646 100644
--- a/internal/server/handlers_apps_shell.go
+++ b/internal/server/handlers_apps_shell.go
@@ -21,6 +21,7 @@ import (
"github.com/charmbracelet/x/xpty"
"github.com/gorilla/websocket"
+ "github.com/opencsgs/csghub-lite/internal/antigravityagent"
"github.com/opencsgs/csghub-lite/internal/claudeagent"
"github.com/opencsgs/csghub-lite/internal/codexagent"
"github.com/opencsgs/csghub-lite/internal/config"
@@ -618,6 +619,12 @@ func resolveAIAppOpenTarget(appID string) (aiAppOpenTarget, error) {
DisplayName: "Pi",
Binaries: []string{"pi"},
}, nil
+ case "antigravity":
+ return aiAppOpenTarget{
+ AppID: "antigravity",
+ DisplayName: "Antigravity",
+ Binaries: []string{"agy"},
+ }, nil
default:
return aiAppOpenTarget{}, fmt.Errorf("%s does not provide a web shell entry yet", appID)
}
@@ -918,6 +925,19 @@ func (s *Server) prepareAIAppShellLaunch(target aiAppOpenTarget, modelID string,
Env: envWithOverridesAndUnset(aiAppShellEnvOverrides(nil), "NO_COLOR"),
Dir: workingDir,
}, nil
+ case "antigravity":
+ models := make([]api.ModelInfo, 0, len(modelIDs))
+ for _, modelID := range modelIDs {
+ models = append(models, api.ModelInfo{Model: modelID})
+ }
+ if err := antigravityagent.SyncConfig(serverURL, openClawProviderAPIKey(s.cfg.Token), modelID, models); err != nil {
+ log.Printf("AI APP antigravity: syncing config failed: %v", err)
+ }
+ return aiAppPreparedLaunch{
+ Binary: binary,
+ Env: envWithOverridesAndUnset(aiAppShellEnvOverrides(antigravityagent.EnvOverrides(serverURL, openClawProviderAPIKey(s.cfg.Token), modelID)), "NO_COLOR"),
+ Dir: workingDir,
+ }, nil
default:
return aiAppPreparedLaunch{}, fmt.Errorf("%s does not support web shell launch yet", target.DisplayName)
}
diff --git a/internal/server/static/apps/antigravity.svg b/internal/server/static/apps/antigravity.svg
new file mode 100644
index 0000000..d124a3e
--- /dev/null
+++ b/internal/server/static/apps/antigravity.svg
@@ -0,0 +1,14 @@
+
diff --git a/local/secrets.env.example b/local/secrets.env.example
index cf4ce5e..d034c16 100644
--- a/local/secrets.env.example
+++ b/local/secrets.env.example
@@ -14,3 +14,5 @@ STARHUB_OPEN_CODE_DIST_PREFIX="open-code-releases"
STARHUB_OPEN_CODE_DIST_BASE_URL="https://${STARHUB_OSS_PUBLIC_BUCKET}.${STARHUB_OSS_ENDPOINT}/${STARHUB_OPEN_CODE_DIST_PREFIX}"
STARHUB_CODEX_DIST_PREFIX="codex-releases"
STARHUB_CODEX_DIST_BASE_URL="https://${STARHUB_OSS_PUBLIC_BUCKET}.${STARHUB_OSS_ENDPOINT}/${STARHUB_CODEX_DIST_PREFIX}"
+STARHUB_ANTIGRAVITY_DIST_PREFIX="antigravity-releases"
+STARHUB_ANTIGRAVITY_DIST_BASE_URL="https://${STARHUB_OSS_PUBLIC_BUCKET}.${STARHUB_OSS_ENDPOINT}/${STARHUB_ANTIGRAVITY_DIST_PREFIX}"
diff --git a/scripts/sync-ai-app-oss.sh b/scripts/sync-ai-app-oss.sh
index 5f21c2d..c6d8701 100755
--- a/scripts/sync-ai-app-oss.sh
+++ b/scripts/sync-ai-app-oss.sh
@@ -29,6 +29,7 @@ Supported apps:
- claude-code
- open-code
- codex
+ - antigravity
Options:
--app APP App to sync (repeatable). Defaults to all mirror-backed apps
@@ -49,10 +50,12 @@ Optional environment variables:
STARHUB_OPEN_CODE_DIST_BASE_URL Public URL override for generated manifest
STARHUB_CODEX_DIST_PREFIX Default: codex-releases
STARHUB_CODEX_DIST_BASE_URL Public URL override for generated manifest
+ STARHUB_ANTIGRAVITY_DIST_PREFIX Default: antigravity-releases
+ STARHUB_ANTIGRAVITY_DIST_BASE_URL Public URL override for generated manifest
Examples:
./scripts/sync-ai-app-oss.sh
- ./scripts/sync-ai-app-oss.sh --app claude-code --app open-code --app codex
+ ./scripts/sync-ai-app-oss.sh --app claude-code --app open-code --app codex --app antigravity
./scripts/sync-ai-app-oss.sh --app codex --version 0.118.0
./scripts/sync-ai-app-oss.sh --app claude-code
EOF
@@ -89,7 +92,7 @@ while [[ $# -gt 0 ]]; do
done
if [[ "${#APP_IDS[@]}" -eq 0 ]]; then
- APP_IDS=(claude-code open-code codex)
+ APP_IDS=(claude-code open-code codex antigravity)
fi
if [[ "$REQUESTED_VERSION" != "latest" && "${#APP_IDS[@]}" -ne 1 ]]; then
@@ -98,7 +101,7 @@ fi
for app_id in "${APP_IDS[@]}"; do
case "${app_id}" in
- claude-code|open-code|codex) ;;
+ claude-code|open-code|codex|antigravity) ;;
*) die "unsupported app: ${app_id}" ;;
esac
done
@@ -170,7 +173,11 @@ download_file() {
local output="$2"
ensure_external_proxy
if command -v curl >/dev/null 2>&1; then
- curl --connect-timeout 15 --max-time 1800 --retry 3 --retry-delay 2 -fsSL -o "$output" "$url"
+ if [[ -f "$output" ]]; then
+ curl --connect-timeout 15 --max-time 1800 --retry 5 --retry-delay 5 --retry-all-errors -C - -fSL -o "$output" "$url"
+ else
+ curl --connect-timeout 15 --max-time 1800 --retry 5 --retry-delay 5 --retry-all-errors -fSL -o "$output" "$url"
+ fi
else
wget --tries=3 --timeout=30 -O "$output" "$url"
fi
@@ -190,6 +197,20 @@ print(h.hexdigest())
PY
}
+sha512_file() {
+ "${PYTHON_BIN}" - "$1" <<'PY'
+import hashlib
+import sys
+
+path = sys.argv[1]
+h = hashlib.sha512()
+with open(path, "rb") as fh:
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
+ h.update(chunk)
+print(h.hexdigest())
+PY
+}
+
require_oss2() {
"${PYTHON_BIN}" - <<'PY' >/dev/null 2>&1
import oss2 # noqa: F401
@@ -272,6 +293,7 @@ app_prefix() {
case "$1" in
open-code) trim_trailing_slash "${STARHUB_OPEN_CODE_DIST_PREFIX:-open-code-releases}" ;;
codex) trim_trailing_slash "${STARHUB_CODEX_DIST_PREFIX:-codex-releases}" ;;
+ antigravity) trim_trailing_slash "${STARHUB_ANTIGRAVITY_DIST_PREFIX:-antigravity-releases}" ;;
*)
die "unsupported release-backed app: $1"
;;
@@ -282,6 +304,7 @@ app_public_base_url() {
case "$1" in
open-code) resolve_public_base_url "$(app_prefix "$1")" "${STARHUB_OPEN_CODE_DIST_BASE_URL:-}" ;;
codex) resolve_public_base_url "$(app_prefix "$1")" "${STARHUB_CODEX_DIST_BASE_URL:-}" ;;
+ antigravity) resolve_public_base_url "$(app_prefix "$1")" "${STARHUB_ANTIGRAVITY_DIST_BASE_URL:-}" ;;
*)
die "unsupported release-backed app: $1"
;;
@@ -345,6 +368,154 @@ sync_claude_via_wrapper() {
"${cmd[@]}"
}
+sync_antigravity_app() {
+ local app_id="antigravity"
+ local prefix=""
+ local public_base_url=""
+ local workdir=""
+ local index_file=""
+ local manifest_file=""
+ local latest_file=""
+ local version=""
+ local object_key=""
+ local artifact_path=""
+ local actual_checksum=""
+
+ if [[ "${REQUESTED_VERSION}" != "latest" ]]; then
+ die "antigravity sync currently supports latest only; upstream exposes latest platform manifests"
+ fi
+
+ prefix="$(app_prefix "${app_id}")"
+ public_base_url="$(app_public_base_url "${app_id}")"
+ workdir="$(mktemp -d "${TMPDIR:-/tmp}/${app_id}-oss-sync.XXXXXX")"
+ if [[ "${KEEP_WORKDIR}" == "1" ]]; then
+ info "kept workdir for ${app_id}: ${workdir}"
+ fi
+
+ index_file="${workdir}/platforms.tsv"
+ manifest_file="${workdir}/manifest.json"
+ latest_file="${workdir}/latest"
+
+ ensure_external_proxy
+ "${PYTHON_BIN}" - "${index_file}" <<'PY'
+import json
+import os
+import sys
+import urllib.request
+
+index_path = sys.argv[1]
+base_url = "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests"
+platforms = [
+ ("darwin-arm64", "darwin_arm64", "tar.gz", "antigravity", "agy"),
+ ("darwin-x64", "darwin_amd64", "tar.gz", "antigravity", "agy"),
+ ("linux-arm64", "linux_arm64", "tar.gz", "antigravity", "agy"),
+ ("linux-x64", "linux_amd64", "tar.gz", "antigravity", "agy"),
+ ("win32-arm64", "windows_arm64", "raw", "agy.exe", "agy.exe"),
+ ("win32-x64", "windows_amd64", "raw", "agy.exe", "agy.exe"),
+]
+
+rows = []
+versions = set()
+for platform, upstream_platform, archive_format, binary, launcher in platforms:
+ url = f"{base_url}/{upstream_platform}.json"
+ with urllib.request.urlopen(url, timeout=60) as resp:
+ manifest = json.load(resp)
+ version = str(manifest["version"]).strip()
+ source_url = str(manifest["url"]).strip()
+ checksum = str(manifest["sha512"]).strip().lower()
+ asset_name = os.path.basename(source_url.split("?", 1)[0])
+ versions.add(version)
+ rows.append((platform, upstream_platform, asset_name, archive_format, binary, launcher, checksum, version, source_url))
+
+if len(versions) != 1:
+ raise SystemExit(f"Antigravity platform manifests reported different versions: {sorted(versions)}")
+
+with open(index_path, "w", encoding="utf-8") as out:
+ for row in rows:
+ out.write("\t".join(row) + "\n")
+PY
+
+ version="$(awk -F $'\t' 'NR == 1 {print $8}' "${index_file}")"
+ [[ -n "${version}" ]] || die "failed to resolve antigravity version"
+ info "syncing antigravity version ${version} from upstream platform manifests"
+
+ while IFS=$'\t' read -r platform upstream_platform asset_name archive_format binary launcher checksum row_version download_url; do
+ [[ -n "$platform" ]] || continue
+
+ artifact_path="${workdir}/${asset_name}"
+ object_key="${prefix}/${version}/${platform}/${asset_name}"
+
+ info "downloading antigravity ${upstream_platform}/${asset_name}"
+ download_file "${download_url}" "${artifact_path}"
+
+ actual_checksum="$(sha512_file "${artifact_path}")"
+ if [[ "${actual_checksum}" != "${checksum}" ]]; then
+ die "checksum mismatch for antigravity ${platform}/${asset_name}: expected ${checksum}, got ${actual_checksum}"
+ fi
+
+ info "uploading ${object_key}"
+ oss_put_object "${artifact_path}" "${object_key}" "application/octet-stream"
+ done < "${index_file}"
+
+ "${PYTHON_BIN}" - "${index_file}" "${manifest_file}" "${version}" "${prefix}" "${public_base_url}" <<'PY'
+import json
+import sys
+from datetime import datetime, timezone
+
+index_path, manifest_path, version, prefix, public_base_url = sys.argv[1:6]
+manifest = {
+ "app_id": "antigravity",
+ "version": version,
+ "source": "antigravity-platform-manifest",
+ "repository": "https://antigravity.google/cli/install.sh",
+ "prefix": prefix,
+ "public_base_url": public_base_url,
+ "synced_at": datetime.now(timezone.utc).isoformat(),
+ "platforms": {},
+}
+
+with open(index_path, "r", encoding="utf-8") as fh:
+ for raw in fh:
+ raw = raw.strip()
+ if not raw:
+ continue
+ platform, upstream_platform, asset_name, archive_format, binary, launcher, checksum, row_version, download_url = raw.split("\t")
+ path = f"{version}/{platform}/{asset_name}"
+ manifest["platforms"][platform] = {
+ "asset": asset_name,
+ "archive_format": archive_format,
+ "binary": binary,
+ "launcher": launcher,
+ "checksum_sha512": checksum,
+ "path": path,
+ "source_platform": upstream_platform,
+ "source_url": download_url,
+ "public_url": f"{public_base_url}/{path}",
+ }
+
+with open(manifest_path, "w", encoding="utf-8") as fh:
+ json.dump(manifest, fh, indent=2, sort_keys=True)
+ fh.write("\n")
+PY
+
+ info "uploading ${prefix}/${version}/manifest.json"
+ oss_put_object "${manifest_file}" "${prefix}/${version}/manifest.json" "application/json"
+
+ if [[ "${UPDATE_LATEST}" == "1" ]]; then
+ printf '%s\n' "${version}" > "${latest_file}"
+ info "uploading ${prefix}/latest"
+ oss_put_object "${latest_file}" "${prefix}/latest" "text/plain"
+ fi
+
+ info "antigravity ${version} mirror is ready"
+ info "public base URL: ${public_base_url}"
+ info "manifest URL: ${public_base_url}/${version}/manifest.json"
+
+ if [[ "${KEEP_WORKDIR}" != "1" ]]; then
+ rm -rf "${workdir}"
+ fi
+}
+
sync_release_app() {
local app_id="$1"
local repo=""
@@ -538,7 +709,15 @@ PY
}
require_cmd python3
-require_cmd gh
+needs_gh=0
+for app_id in "${APP_IDS[@]}"; do
+ case "${app_id}" in
+ open-code|codex) needs_gh=1 ;;
+ esac
+done
+if [[ "${needs_gh}" == "1" ]]; then
+ require_cmd gh
+fi
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
die "curl or wget is required"
fi
@@ -567,6 +746,9 @@ for app_id in "${APP_IDS[@]}"; do
open-code|codex)
sync_release_app "${app_id}"
;;
+ antigravity)
+ sync_antigravity_app
+ ;;
*)
die "unsupported app: ${app_id}"
;;
diff --git a/scripts/sync-claude-code-oss.sh b/scripts/sync-claude-code-oss.sh
index 4b6fcee..7533af1 100755
--- a/scripts/sync-claude-code-oss.sh
+++ b/scripts/sync-claude-code-oss.sh
@@ -150,8 +150,31 @@ bucket_endpoint_url() {
printf 'https://%s.%s\n' "${STARHUB_OSS_PUBLIC_BUCKET}" "$host"
}
+ensure_external_proxy() {
+ local before=""
+ local after=""
+ before="${https_proxy:-}${HTTPS_PROXY:-}${http_proxy:-}${HTTP_PROXY:-}"
+ if [[ -n "$before" ]]; then
+ return 0
+ fi
+ if [[ -z "${HOME:-}" || ! -f "${HOME}/.myshrc" ]]; then
+ return 0
+ fi
+
+ set +u
+ # shellcheck source=/dev/null
+ . "${HOME}/.myshrc" >/dev/null 2>&1 || true
+ set -u
+
+ after="${https_proxy:-}${HTTPS_PROXY:-}${http_proxy:-}${HTTP_PROXY:-}"
+ if [[ -n "$after" ]]; then
+ printf '\033[0;32m[INFO]\033[0m %s\n' "loaded proxy settings from ${HOME}/.myshrc for upstream downloads" >&2
+ fi
+}
+
download_text() {
local url="$1"
+ ensure_external_proxy
if command -v curl >/dev/null 2>&1; then
curl --connect-timeout 15 --max-time 60 --retry 3 --retry-delay 2 -fsSL "$url"
else
@@ -162,12 +185,19 @@ download_text() {
download_file() {
local url="$1"
local output="$2"
+ ensure_external_proxy
if [[ "$url" == https://storage.googleapis.com/* ]]; then
- parallel_range_download_file "$url" "$output"
- return 0
+ if parallel_range_download_file "$url" "$output"; then
+ return 0
+ fi
+ warn "range download unavailable for ${url}; falling back to curl"
fi
if command -v curl >/dev/null 2>&1; then
- curl --connect-timeout 15 --max-time 1800 --retry 3 --retry-delay 2 -fsSL -o "$output" "$url"
+ if [[ -f "$output" ]]; then
+ curl --connect-timeout 15 --max-time 1800 --retry 5 --retry-delay 5 -C - -fSL -o "$output" "$url"
+ else
+ curl --connect-timeout 15 --max-time 1800 --retry 5 --retry-delay 5 -fSL -o "$output" "$url"
+ fi
else
wget --tries=3 --timeout=30 -O "$output" "$url"
fi
@@ -187,7 +217,7 @@ import urllib.request
url, output = sys.argv[1:3]
part_path = output + ".part"
workers = int(os.environ.get("CLAUDE_CODE_DOWNLOAD_WORKERS", "8"))
-chunk_size = int(os.environ.get("CLAUDE_CODE_DOWNLOAD_CHUNK_SIZE", str(16 * 1024 * 1024)))
+chunk_size = int(os.environ.get("CLAUDE_CODE_DOWNLOAD_CHUNK_SIZE", str(2 * 1024 * 1024)))
def request(method, headers=None, timeout=60):
diff --git a/web/public/apps/antigravity.svg b/web/public/apps/antigravity.svg
new file mode 100644
index 0000000..d124a3e
--- /dev/null
+++ b/web/public/apps/antigravity.svg
@@ -0,0 +1,14 @@
+
diff --git a/web/src/data/aiApps.ts b/web/src/data/aiApps.ts
index a1dc5e1..9519653 100644
--- a/web/src/data/aiApps.ts
+++ b/web/src/data/aiApps.ts
@@ -240,6 +240,50 @@ export const aiAppsCatalog: AIAppCatalogEntry[] = [
zh: "可安装",
},
},
+ {
+ id: "antigravity",
+ name: "Antigravity",
+ siteLabel: "@google",
+ website: "https://antigravity.google",
+ detailsUrl: "https://antigravity.google",
+ icon: "/apps/antigravity.svg",
+ category: "coding",
+ description: {
+ en: "Google's AI coding agent CLI for working with projects from the terminal.",
+ zh: "Google 的 AI 编程 Agent CLI,可在终端中协助处理项目代码。",
+ },
+ installMode: "script",
+ progressMode: "percent",
+ installHint: {
+ en: "Install the mirrored Antigravity CLI release for macOS, Linux, and Windows.",
+ zh: "通过镜像的 Antigravity CLI 发布包完成安装,支持 macOS、Linux 和 Windows。",
+ },
+ cnInstallHint: {
+ en: "By default the installer reads a versioned Antigravity mirror and wires the agy launcher locally; set CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL only when testing another mirror.",
+ zh: "默认从版本化的 Antigravity 镜像读取发布包并在本地配置 agy 启动命令;仅在测试其他镜像时才需要设置 CSGHUB_LITE_ANTIGRAVITY_DIST_BASE_URL。",
+ },
+ commandPreview: "curl -fsSL https://antigravity.google/cli/install.sh | bash",
+ liveLogsReady: true,
+ plannedSteps: [
+ {
+ en: "Resolve the requested Antigravity CLI version from the mirrored manifest.",
+ zh: "从镜像 manifest 中解析目标 Antigravity CLI 版本。",
+ },
+ {
+ en: "Download the mirrored release for the current platform and verify its SHA512 checksum.",
+ zh: "下载当前平台对应的镜像发布包,并校验 SHA512 checksum。",
+ },
+ {
+ en: "Place the agy launcher in the user's local bin directory and run Antigravity's shell environment setup.",
+ zh: "将 agy 启动命令安装到当前用户本地 bin 目录,并执行 Antigravity 的 shell 环境配置。",
+ },
+ ],
+ status: "idle",
+ statusText: {
+ en: "Ready to install",
+ zh: "可安装",
+ },
+ },
{
id: "pi",
name: "Pi",
diff --git a/web/src/i18n.ts b/web/src/i18n.ts
index e5e412e..a2a8fe1 100644
--- a/web/src/i18n.ts
+++ b/web/src/i18n.ts
@@ -243,6 +243,9 @@ const en: Record = {
"aiApps.phase.detecting_platform": "Detecting platform",
"aiApps.phase.resolving_latest": "Resolving latest version",
"aiApps.phase.downloading_binary": "Downloading binary",
+ "aiApps.phase.downloading_archive": "Downloading release package",
+ "aiApps.phase.verifying_checksum": "Verifying checksum",
+ "aiApps.phase.installing_runtime": "Installing runtime",
"aiApps.phase.installing": "Installing package",
"aiApps.phase.running_installer": "Running installer",
"aiApps.phase.removing_package": "Removing package",
@@ -1050,6 +1053,9 @@ const zh: Record = {
"aiApps.phase.detecting_platform": "识别平台",
"aiApps.phase.resolving_latest": "解析最新版本",
"aiApps.phase.downloading_binary": "下载二进制",
+ "aiApps.phase.downloading_archive": "下载发布包",
+ "aiApps.phase.verifying_checksum": "校验 checksum",
+ "aiApps.phase.installing_runtime": "安装运行时",
"aiApps.phase.installing": "安装依赖包",
"aiApps.phase.running_installer": "运行安装器",
"aiApps.phase.removing_package": "移除全局包",
diff --git a/web/src/pages/AIAppShell.tsx b/web/src/pages/AIAppShell.tsx
index 966094f..7d9eaac 100644
--- a/web/src/pages/AIAppShell.tsx
+++ b/web/src/pages/AIAppShell.tsx
@@ -8,7 +8,7 @@ import { locale, t } from "../i18n";
type ConnectionState = "connecting" | "connected" | "disconnected" | "exited";
const claudeCodeAppId = "claude-code";
const shellAppsWithModelSwitch = new Set([claudeCodeAppId, "pi"]);
-const shellAppsWithWorkDirSwitch = new Set(["claude-code", "open-code", "codex", "pi"]);
+const shellAppsWithWorkDirSwitch = new Set(["claude-code", "open-code", "codex", "antigravity", "pi"]);
interface ShellControlMessage {
type: string;
diff --git a/web/src/pages/AIApps.tsx b/web/src/pages/AIApps.tsx
index 3807afd..84d86e8 100644
--- a/web/src/pages/AIApps.tsx
+++ b/web/src/pages/AIApps.tsx
@@ -1355,7 +1355,15 @@ function updateStatusLabel(state: AIAppRuntimeState): string {
function phaseLabel(phase: string): string {
const key = `aiApps.phase.${phase}`;
const translated = t(key);
- return translated === key ? phase : translated;
+ return translated === key ? humanizePhase(phase) : translated;
+}
+
+function humanizePhase(phase: string): string {
+ return phase
+ .split(/[_-]+/)
+ .filter(Boolean)
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(" ");
}
function statusColorClass(status: AIAppRuntimeState["status"]): string {
@@ -1405,7 +1413,7 @@ function drawerNotice(state: AIAppRuntimeState): string {
}
function canOpenAIApp(app: AIAppCatalogEntry, state: AIAppRuntimeState): boolean {
- return ["openclaw", "csgclaw", "claude-code", "open-code", "codex", "pi"].includes(app.id) &&
+ return ["openclaw", "csgclaw", "claude-code", "open-code", "codex", "antigravity", "pi"].includes(app.id) &&
state.status === "installed" &&
!state.disabled;
}
@@ -1451,6 +1459,8 @@ function cliLaunchAppName(appID: string): string {
return "opencode";
case "codex":
return "codex";
+ case "antigravity":
+ return "antigravity";
case "pi":
return "pi";
case "openclaw":
@@ -1463,7 +1473,7 @@ function cliLaunchAppName(appID: string): string {
}
function canSelectAIAppModel(app: AIAppCatalogEntry): boolean {
- return ["claude-code", "open-code", "codex", "pi", "openclaw", "csgclaw"].includes(app.id);
+ return ["claude-code", "open-code", "codex", "antigravity", "pi", "openclaw", "csgclaw"].includes(app.id);
}
function normalizeAIAppModels(models: ModelInfo[]): ModelInfo[] {