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 @@ + + Antigravity + + + + + + + + + + + + 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 @@ + + Antigravity + + + + + + + + + + + + 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[] {