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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/agent-guidelines/ai-app-oss-mirror.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/guides/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

### 同步工作流

Expand All @@ -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. **逐个同步应用**:每个应用下载需要几分钟,建议单独同步以避免超时。
Expand All @@ -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` 与上游版本一致,无需重新同步。
Expand Down Expand Up @@ -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 补发

Expand Down
135 changes: 135 additions & 0 deletions internal/antigravityagent/config.go
Original file line number Diff line number Diff line change
@@ -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)
}
75 changes: 75 additions & 0 deletions internal/antigravityagent/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
26 changes: 26 additions & 0 deletions internal/apps/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions internal/apps/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
Loading