diff --git a/quickstart.ps1 b/quickstart.ps1 index b8bbd9a254..ded9543f15 100644 --- a/quickstart.ps1 +++ b/quickstart.ps1 @@ -1022,6 +1022,10 @@ $hiveKey = [System.Environment]::GetEnvironmentVariable("HIVE_API_KEY", "User") if (-not $hiveKey) { $hiveKey = $env:HIVE_API_KEY } if ($hiveKey) { $HiveCredDetected = $true } +$AntigravityCredDetected = $false +$antigravityAuthPath = Join-Path $env:USERPROFILE ".hive\antigravity-accounts.json" +if (Test-Path $antigravityAuthPath) { $AntigravityCredDetected = $true } + # Detect API key providers $ProviderMenuEnvVars = @("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY", "CEREBRAS_API_KEY", "OPENROUTER_API_KEY") $ProviderMenuNames = @("Anthropic (Claude) - Recommended", "OpenAI (GPT)", "Google Gemini - Free tier available", "Groq - Fast, free tier", "Cerebras - Fast, free tier", "OpenRouter - Bring any OpenRouter model") @@ -1057,6 +1061,7 @@ if (Test-Path $HiveConfigFile) { if ($prevLlm.use_claude_code_subscription) { $PrevSubMode = "claude_code" } elseif ($prevLlm.use_codex_subscription) { $PrevSubMode = "codex" } elseif ($prevLlm.use_kimi_code_subscription) { $PrevSubMode = "kimi_code" } + elseif ($prevLlm.use_antigravity_subscription) { $PrevSubMode = "antigravity" } elseif ($prevLlm.api_base -and $prevLlm.api_base -like "*api.z.ai*") { $PrevSubMode = "zai_code" } elseif ($prevLlm.provider -eq "minimax" -or ($prevLlm.api_base -and $prevLlm.api_base -like "*api.minimax.io*")) { $PrevSubMode = "minimax_code" } elseif ($prevLlm.api_base -and $prevLlm.api_base -like "*api.kimi.com*") { $PrevSubMode = "kimi_code" } @@ -1076,6 +1081,7 @@ if ($PrevSubMode -or $PrevProvider) { "minimax_code" { if ($MinimaxCredDetected) { $prevCredValid = $true } } "kimi_code" { if ($KimiCredDetected) { $prevCredValid = $true } } "hive_llm" { if ($HiveCredDetected) { $prevCredValid = $true } } + "antigravity" { if ($AntigravityCredDetected) { $prevCredValid = $true } } default { if ($PrevProvider -eq "ollama") { $prevCredValid = $true @@ -1094,18 +1100,20 @@ if ($PrevSubMode -or $PrevProvider) { "minimax_code" { $DefaultChoice = "4" } "kimi_code" { $DefaultChoice = "5" } "hive_llm" { $DefaultChoice = "6" } + "antigravity" { $DefaultChoice = "7" } } if (-not $DefaultChoice) { switch ($PrevProvider) { - "anthropic" { $DefaultChoice = "7" } - "openai" { $DefaultChoice = "8" } - "gemini" { $DefaultChoice = "9" } - "groq" { $DefaultChoice = "10" } - "cerebras" { $DefaultChoice = "11" } - "openrouter" { $DefaultChoice = "12" } - "ollama" { $DefaultChoice = "13" } + "anthropic" { $DefaultChoice = "8" } + "openai" { $DefaultChoice = "9" } + "gemini" { $DefaultChoice = "10" } + "groq" { $DefaultChoice = "11" } + "cerebras" { $DefaultChoice = "12" } + "openrouter" { $DefaultChoice = "13" } + "ollama" { $DefaultChoice = "14" } "minimax" { $DefaultChoice = "4" } "kimi" { $DefaultChoice = "5" } + "hive" { $DefaultChoice = "6" } } } } @@ -1158,12 +1166,19 @@ Write-Host ") Hive LLM " -NoNewline Write-Color -Text "(use your Hive API key)" -Color DarkGray -NoNewline if ($HiveCredDetected) { Write-Color -Text " (credential detected)" -Color Green } else { Write-Host "" } +# 7) Antigravity +Write-Host " " -NoNewline +Write-Color -Text "7" -Color Cyan -NoNewline +Write-Host ") Antigravity Subscription " -NoNewline +Write-Color -Text "(use your Google/Gemini plan)" -Color DarkGray -NoNewline +if ($AntigravityCredDetected) { Write-Color -Text " (credential detected)" -Color Green } else { Write-Host "" } + Write-Host "" Write-Color -Text " API key providers:" -Color Cyan -# 7-12) API key providers +# 8-13) API key providers for ($idx = 0; $idx -lt $ProviderMenuEnvVars.Count; $idx++) { - $num = $idx + 7 + $num = $idx + 8 $envVal = [System.Environment]::GetEnvironmentVariable($ProviderMenuEnvVars[$idx], "Process") if (-not $envVal) { $envVal = [System.Environment]::GetEnvironmentVariable($ProviderMenuEnvVars[$idx], "User") } Write-Host " " -NoNewline @@ -1172,9 +1187,9 @@ for ($idx = 0; $idx -lt $ProviderMenuEnvVars.Count; $idx++) { if ($envVal) { Write-Color -Text " (credential detected)" -Color Green } else { Write-Host "" } } -# 13) Local (Ollama) — no API key needed +# 14) Local (Ollama) - no API key needed Write-Host " " -NoNewline -Write-Color -Text "13" -Color Cyan -NoNewline +Write-Color -Text "14" -Color Cyan -NoNewline if ($OllamaDetected) { Write-Host ") Local (Ollama) - No API key needed " -NoNewline Write-Color -Text "(ollama detected)" -Color Green @@ -1182,7 +1197,7 @@ if ($OllamaDetected) { Write-Host ") Local (Ollama) - No API key needed" } -$SkipChoice = 7 + $ProviderMenuEnvVars.Count + 1 +$SkipChoice = 8 + $ProviderMenuEnvVars.Count + 1 Write-Host " " -NoNewline Write-Color -Text "$SkipChoice" -Color Cyan -NoNewline Write-Host ") Skip for now" @@ -1320,9 +1335,48 @@ switch ($num) { } Write-Color -Text " Model: $SelectedModel | API: $HiveLlmEndpoint" -Color DarkGray } - { $_ -ge 7 -and $_ -le 12 } { + 7 { + # Antigravity Subscription + if (-not $AntigravityCredDetected) { + Write-Host "" + Write-Color -Text " Setting up Antigravity authentication..." -Color Cyan + Write-Host "" + Write-Warn "A browser window will open for Google OAuth." + Write-Host " Sign in with your Google account that has Antigravity access." + Write-Host "" + try { + $null = & $UvCmd run python (Join-Path $ScriptDir "core\antigravity_auth.py") auth account add 2>&1 + if ($LASTEXITCODE -eq 0 -and (Test-Path $antigravityAuthPath)) { + $AntigravityCredDetected = $true + } + } catch { + $AntigravityCredDetected = $false + } + + if (-not $AntigravityCredDetected) { + Write-Host "" + Write-Fail "Authentication failed or was cancelled." + Write-Host "" + $SelectedProviderId = "" + } + } + + if ($AntigravityCredDetected) { + $SubscriptionMode = "antigravity" + $SelectedProviderId = "openai" + $SelectedModel = "gemini-3-flash" + $SelectedMaxTokens = 32768 + $SelectedMaxContextTokens = 1000000 + Write-Host "" + Write-Warn "Using Antigravity can technically cause your account suspension. Please use at your own risk." + Write-Host "" + Write-Ok "Using Antigravity subscription" + Write-Color -Text " Model: gemini-3-flash | Direct OAuth (no proxy required)" -Color DarkGray + } + } + { $_ -ge 8 -and $_ -le 13 } { # API key providers - $provIdx = $num - 7 + $provIdx = $num - 8 $SelectedEnvVar = $ProviderMenuEnvVars[$provIdx] $SelectedProviderId = $ProviderMenuIds[$provIdx] $providerName = $ProviderMenuNames[$provIdx] -replace ' - .*', '' # strip description @@ -1402,7 +1456,7 @@ switch ($num) { } } } - 13 { + 14 { # Local (Ollama) if (-not $OllamaDetected) { Write-Host "" @@ -1774,6 +1828,8 @@ if ($SelectedProviderId) { $config.llm["use_claude_code_subscription"] = $true } elseif ($SubscriptionMode -eq "codex") { $config.llm["use_codex_subscription"] = $true + } elseif ($SubscriptionMode -eq "antigravity") { + $config.llm["use_antigravity_subscription"] = $true } elseif ($SubscriptionMode -eq "zai_code") { $config.llm["api_base"] = "https://api.z.ai/api/coding/paas/v4" $config.llm["api_key_env_var"] = $SelectedEnvVar @@ -2096,6 +2152,9 @@ if ($SelectedProviderId) { Write-Color -Text " API: api.minimax.io/v1 (OpenAI-compatible)" -Color DarkGray } elseif ($SubscriptionMode -eq "codex") { Write-Ok "OpenAI Codex Subscription -> $SelectedModel" + } elseif ($SubscriptionMode -eq "antigravity") { + Write-Ok "Antigravity Subscription -> $SelectedModel" + Write-Color -Text " Direct OAuth (no proxy required)" -Color DarkGray } elseif ($SelectedProviderId -eq "openrouter") { Write-Ok "OpenRouter API Key -> $SelectedModel" Write-Color -Text " API: openrouter.ai/api/v1 (OpenAI-compatible)" -Color DarkGray diff --git a/quickstart.sh b/quickstart.sh index c4669eb54e..0edb2d8c88 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -673,7 +673,18 @@ detect_shell_rc() { fi ;; bash) - if [ -f "$HOME/.bashrc" ]; then + # Git Bash on Windows commonly starts as a login shell, so prefer + # .bash_profile there when it already exists. On Unix-like shells, + # keep the traditional .bashrc-first behavior. + if [ -n "$MSYSTEM" ] || [ -n "$MINGW_PREFIX" ]; then + if [ -f "$HOME/.bash_profile" ]; then + echo "$HOME/.bash_profile" + elif [ -f "$HOME/.bashrc" ]; then + echo "$HOME/.bashrc" + else + echo "$HOME/.profile" + fi + elif [ -f "$HOME/.bashrc" ]; then echo "$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then echo "$HOME/.bash_profile" diff --git a/tools/src/aden_tools/credentials/shell_config.py b/tools/src/aden_tools/credentials/shell_config.py index f1e33aa294..c30fc1a787 100644 --- a/tools/src/aden_tools/credentials/shell_config.py +++ b/tools/src/aden_tools/credentials/shell_config.py @@ -1,13 +1,15 @@ """ Shell configuration utilities for persisting environment variables. -Supports both bash and zsh, detecting the user's default shell. +Supports bash and zsh with platform-aware fallbacks for login-shell config +files such as ``.bash_profile``, ``.zshenv``, and ``.profile``. Used primarily for persisting ADEN_API_KEY across sessions. """ from __future__ import annotations import os +import platform import re from pathlib import Path from typing import Literal @@ -34,9 +36,9 @@ def detect_shell() -> ShellType: else: # Try to detect from config file existence home = Path.home() - if (home / ".zshrc").exists(): + if (home / ".zshrc").exists() or (home / ".zshenv").exists(): return "zsh" - elif (home / ".bashrc").exists(): + elif (home / ".bashrc").exists() or (home / ".bash_profile").exists(): return "bash" return "unknown" @@ -55,14 +57,12 @@ def get_shell_config_path(shell_type: ShellType | None = None) -> Path: shell_type = detect_shell() home = Path.home() + candidates = _get_shell_config_candidates(home, shell_type) - if shell_type == "zsh": - return home / ".zshrc" - elif shell_type == "bash": - return home / ".bashrc" - else: - # Default to .bashrc for unknown shells - return home / ".bashrc" + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] def check_env_var_in_shell_config( @@ -79,29 +79,47 @@ def check_env_var_in_shell_config( Returns: Tuple of (exists, current_value or None) """ - config_path = get_shell_config_path(shell_type) + if shell_type is None: + shell_type = detect_shell() - if not config_path.exists(): - return False, None + for config_path in _get_shell_config_candidates(Path.home(), shell_type): + if not config_path.exists(): + continue - content = config_path.read_text(encoding="utf-8") + content = config_path.read_text(encoding="utf-8") - # Look for export ENV_VAR=value or export ENV_VAR="value" - pattern = rf"^export\s+{re.escape(env_var)}=(.+)$" - match = re.search(pattern, content, re.MULTILINE) + # Look for export ENV_VAR=value or export ENV_VAR="value" + pattern = rf"^export\s+{re.escape(env_var)}=(.+)$" + match = re.search(pattern, content, re.MULTILINE) - if match: - value = match.group(1).strip() - # Remove surrounding quotes if present - if (value.startswith('"') and value.endswith('"')) or ( - value.startswith("'") and value.endswith("'") - ): - value = value[1:-1] - return True, value + if match: + value = match.group(1).strip() + # Remove surrounding quotes if present + if (value.startswith('"') and value.endswith('"')) or ( + value.startswith("'") and value.endswith("'") + ): + value = value[1:-1] + return True, value return False, None +def _get_shell_config_candidates(home: Path, shell_type: ShellType) -> list[Path]: + """Return candidate config files in lookup order for the detected shell.""" + if shell_type == "zsh": + return [home / ".zshrc", home / ".zshenv"] + + if shell_type == "bash": + # Git Bash commonly launches login shells on Windows, so prefer + # ``.bash_profile`` there for writes, but keep ``.bashrc`` in the + # lookup list so older setups continue to work. + if platform.system() == "Windows": + return [home / ".bash_profile", home / ".bashrc", home / ".profile"] + return [home / ".bashrc", home / ".bash_profile", home / ".profile"] + + return [home / ".profile", home / ".bashrc"] + + def add_env_var_to_shell_config( env_var: str, value: str, diff --git a/tools/tests/test_shell_config.py b/tools/tests/test_shell_config.py new file mode 100644 index 0000000000..3c276ec6ab --- /dev/null +++ b/tools/tests/test_shell_config.py @@ -0,0 +1,90 @@ +"""Tests for shell config path selection and env-var lookups.""" + +from pathlib import Path + +from aden_tools.credentials import shell_config + + +def _mock_home(monkeypatch, tmp_path: Path) -> None: + monkeypatch.setattr(shell_config.Path, "home", staticmethod(lambda: tmp_path)) + + +def test_get_shell_config_path_prefers_existing_bash_profile(monkeypatch, tmp_path): + _mock_home(monkeypatch, tmp_path) + monkeypatch.setenv("SHELL", "/usr/bin/bash") + monkeypatch.setattr(shell_config.platform, "system", lambda: "Windows") + + (tmp_path / ".bashrc").write_text("# bashrc\n", encoding="utf-8") + (tmp_path / ".bash_profile").write_text("# bash profile\n", encoding="utf-8") + + assert shell_config.get_shell_config_path() == tmp_path / ".bash_profile" + + +def test_get_shell_config_path_prefers_bashrc_for_non_windows_bash(monkeypatch, tmp_path): + _mock_home(monkeypatch, tmp_path) + monkeypatch.setenv("SHELL", "/usr/bin/bash") + monkeypatch.setattr(shell_config.platform, "system", lambda: "Linux") + + (tmp_path / ".bashrc").write_text("# bashrc\n", encoding="utf-8") + (tmp_path / ".bash_profile").write_text("# bash profile\n", encoding="utf-8") + + assert shell_config.get_shell_config_path() == tmp_path / ".bashrc" + + +def test_check_env_var_in_shell_config_reads_bash_profile(monkeypatch, tmp_path): + _mock_home(monkeypatch, tmp_path) + monkeypatch.setenv("SHELL", "/usr/bin/bash") + monkeypatch.setattr(shell_config.platform, "system", lambda: "Windows") + + (tmp_path / ".bash_profile").write_text( + 'export HIVE_API_KEY="hive-key-123"\n', + encoding="utf-8", + ) + + assert shell_config.check_env_var_in_shell_config("HIVE_API_KEY") == ( + True, + "hive-key-123", + ) + + +def test_check_env_var_in_shell_config_falls_back_to_bashrc_on_windows(monkeypatch, tmp_path): + _mock_home(monkeypatch, tmp_path) + monkeypatch.setenv("SHELL", "/usr/bin/bash") + monkeypatch.setattr(shell_config.platform, "system", lambda: "Windows") + + (tmp_path / ".bash_profile").write_text("# no key here\n", encoding="utf-8") + (tmp_path / ".bashrc").write_text( + 'export HIVE_API_KEY="hive-key-from-bashrc"\n', + encoding="utf-8", + ) + + assert shell_config.check_env_var_in_shell_config("HIVE_API_KEY") == ( + True, + "hive-key-from-bashrc", + ) + + +def test_check_env_var_in_shell_config_reads_zshenv_when_zshrc_missing(monkeypatch, tmp_path): + _mock_home(monkeypatch, tmp_path) + monkeypatch.setenv("SHELL", "/bin/zsh") + monkeypatch.setattr(shell_config.platform, "system", lambda: "Darwin") + + (tmp_path / ".zshenv").write_text( + "export OPENROUTER_API_KEY='or-key-123'\n", + encoding="utf-8", + ) + + assert shell_config.check_env_var_in_shell_config("OPENROUTER_API_KEY") == ( + True, + "or-key-123", + ) + + +def test_get_shell_config_path_falls_back_to_profile_for_unknown_shell(monkeypatch, tmp_path): + _mock_home(monkeypatch, tmp_path) + monkeypatch.setenv("SHELL", "/usr/bin/fish") + monkeypatch.setattr(shell_config.platform, "system", lambda: "Linux") + + (tmp_path / ".profile").write_text("# profile\n", encoding="utf-8") + + assert shell_config.get_shell_config_path() == tmp_path / ".profile"