Skip to content
Merged
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
3 changes: 2 additions & 1 deletion env/mcp/xcodebuild.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"platforms": [
"claude",
"codex"
"codex",
"codebuddy"
]
}
2 changes: 1 addition & 1 deletion env/platforms/claude.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"ANTHROPIC_BASE_URL": "${claude.url}",
"CLAUDE_CODE_EFFORT_LEVEL": "medium",
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001-thinking"
},
Expand Down
4 changes: 2 additions & 2 deletions env/platforms/cline.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
},
"globalState": {
"openAiBaseUrl": "${cline.url}",
"planModeOpenAiModelId": "deepseek-ai/deepseek-v4-pro",
"actModeOpenAiModelId": "deepseek-ai/deepseek-v4-flash"
"planModeOpenAiModelId": "deepseek-v4-pro",
"actModeOpenAiModelId": "deepseek-v4-flash"
},
"secrets": {
"openAiApiKey": "${cline.key}"
Expand Down
45 changes: 31 additions & 14 deletions env/platforms/codebuddy.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,57 @@
{
"_comment": "CodeBuddy model configuration synced when CodeBuddy is installed. API model fields sync by default; set api.enabled=false to disable API sync and clear the managed availableModels list.",
"api": {
"enabled": false
"enabled": true
},
"models": [
{
"id": "deepseek-v4-pro",
"name": "DeepSeek V4 Pro",
"id": "deepseek-v4-pro-of",
"name": "deepseek-v4-pro-of",
"vendor": "dataeyes",
"url": "${codebuddy.url}",
"apiKey": "${codebuddy.key}",
"maxInputTokens": 128000,
"maxOutputTokens": 8192,
"supportsToolCall": true,
"supportsImages": false,
"relatedModels": {
"lite": "deepseek-v4-flash",
"reasoning": "deepseek-v4-pro"
}
"supportsImages": false
},
{
"id": "deepseek-v4-flash",
"name": "DeepSeek V4 Flash",
"id": "deepseek-v4-flash-0731",
"name": "deepseek-v4-flash-0731",
"vendor": "dataeyes",
"url": "${codebuddy.url}",
"apiKey": "${codebuddy.key}",
"maxInputTokens": 128000,
"maxOutputTokens": 8192,
"supportsToolCall": true,
"supportsImages": false
},
{
"id": "grok-4.6",
"name": "grok-4.6",
"vendor": "dataeyes",
"url": "${codebuddy.url}",
"apiKey": "${codebuddy.key}",
"maxInputTokens": 128000,
"maxOutputTokens": 8192,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true
},
{
"id": "gemini-3.5-flash",
"name": "gemini-3.5-flash",
"vendor": "dataeyes",
"url": "${codebuddy.url}",
"apiKey": "${codebuddy.key}",
"maxInputTokens": 128000,
"maxOutputTokens": 8192,
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true
}
],
"availableModels": [
"deepseek-v4-pro",
"deepseek-v4-flash"
],
"availableModels": [],
"preamble": {
"target": "CODEBUDDY.md",
"mode": "full",
Expand Down
4 changes: 2 additions & 2 deletions env/platforms/codex.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"_comment": "Codex platform config — lean team-shared set (core + security/sandbox). Third-party dataeyes API sync is enabled by default; set api.enabled=false to disable API sync and remove the managed model_provider / preferred_auth_method / model_providers block and the DATAEYES_API_KEY env export. MCP servers and the preamble are independent of API sync and always sync. Per-developer preference knobs (reasoning effort, verbosity, personality, features, history, tui, analytics, etc.) are intentionally NOT synced.",
"api": {
"enabled": false
"enabled": true
},
"model": "gpt-5.5",
"model": "gpt-5.6-sol",
"sandbox_mode": "workspace-write",
"approval_policy": "on-request",
"allow_login_shell": true,
Expand Down
2 changes: 1 addition & 1 deletion env/platforms/gemini.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"enabled": true
},
"model": {
"name": "gemini-3.5-flash",
"name": "gemini-3.6-flash",
"maxSessionTurns": -1,
"compressionThreshold": 0.5,
"skipNextSpeakerCheck": true
Expand Down
10 changes: 10 additions & 0 deletions sync/core/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ def xcode_gemini_dir() -> Path:
return xcode_coding_assistant_dir() / "gemini"


def xcode_gemini_dotgemini_dir() -> Path:
"""Xcode Gemini nested .gemini directory (holds GEMINI.md symlink + .env)."""
return xcode_gemini_dir() / ".gemini"


def xcode_gemini_env_path() -> Path:
"""Xcode Gemini .env file path inside the nested .gemini directory."""
return xcode_gemini_dotgemini_dir() / ".env"


# ── Standard tool config paths ───────────────────────────────────────────────

def codex_config_path() -> Path:
Expand Down
6 changes: 5 additions & 1 deletion sync/platforms/codebuddy.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ def _merge_available_models(

- Config-managed IDs appear first and replace any existing duplicates.
- User-added IDs not in the config are preserved after config entries.
- An explicitly empty list from config means "clear all managed models".
"""
if not config_available:
return existing_available
# Config explicitly set availableModels to [] — sync the empty list.
# (The caller already guards against the None case, so an empty list
# here means "set to empty", not "no config".)
return []

config_ids = set(config_available)
# User-added IDs not managed by our config
Expand Down
75 changes: 75 additions & 0 deletions sync/platforms/gemini.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
from pathlib import Path
from typing import Any

import os

from core.common import (
api_enabled as _api_enabled,
load_secrets,
prune_managed_keys_via_sidecar,
read_json_object,
resolve_secrets,
write_json,
)
from core.paths import (
gemini_root_dir,
gemini_settings_path,
xcode_coding_assistant_exists,
xcode_gemini_dir,
xcode_gemini_dotgemini_dir,
xcode_gemini_env_path,
)

# Internal/platform keys that should NOT appear in the managed settings.json.
Expand Down Expand Up @@ -74,6 +80,69 @@ def _sync_settings(
prune_managed_keys_via_sidecar(path, set(managed_settings.keys()), sidecar_path)


def _ensure_gemini_md_symlink(dotgemini: Path) -> None:
"""Step 3: symlink ~/.gemini/GEMINI.md into the Xcode .gemini dir.

A symlink (rather than a copy) keeps a single source of truth: edits to the
user's ~/.gemini/GEMINI.md propagate automatically. We gracefully handle an
existing symlink (refresh target), an existing real file (leave it untouched
unless it already points at the source), and a missing source (warn + skip).
"""
source = gemini_root_dir() / "GEMINI.md"
if not source.exists():
print("[gemini] ~/.gemini/GEMINI.md not found — skipping GEMINI.md symlink for Xcode.")
return
link = dotgemini / "GEMINI.md"
# Already a correct symlink to the source: nothing to do.
if link.is_symlink() and os.path.realpath(link) == os.path.realpath(source):
return
# Replace any existing file/symlink at the target.
if link.exists() or link.is_symlink():
link.unlink()
os.symlink(source, link)
print(f"[gemini] Symlinked GEMINI.md -> {link} (source: {source}).")


def _sync_xcode_env_file(dotgemini: Path, cfg: dict[str, Any]) -> None:
"""Step 4: create .env in the Xcode .gemini dir from export_env_to_zshrc.

The platform config's ``export_env_to_zshrc`` block holds {VAR: value} pairs
(values may contain ${secret} placeholders). We resolve placeholders against
env/secrets.json, then write/merge them into the .env file as KEY="value"
lines. Existing lines NOT managed by the sync (any other keys) are preserved.
"""
export = cfg.get("export_env_to_zshrc")
if not isinstance(export, dict) or not export:
print("[gemini] No export_env_to_zshrc block — skipping .env for Xcode.")
return

secrets = load_secrets()
resolved = resolve_secrets(export, secrets)
unresolved = [v for v in resolved.values() if isinstance(v, str) and "${" in v]
if unresolved:
print("[gemini] ⚠ .env: unresolved placeholders remain — check env/secrets.json.")

env_path = dotgemini / ".env"
existing: dict[str, str] = {}
if env_path.is_file():
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
existing[key.strip()] = val.strip().strip('"').strip("'")

# Overlay managed vars on top of any developer-customized ones.
merged = dict(existing)
for k, v in resolved.items():
if isinstance(v, str):
merged[k] = v

lines = [f'{k}="{v}"' for k, v in merged.items()]
env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"[gemini] Synced .env for Xcode at {env_path} ({len(merged)} vars).")


def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None:
"""Sync MCP servers and platform config to Gemini CLI (native + Xcode).

Expand Down Expand Up @@ -114,3 +183,9 @@ def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None:
mcp_servers,
xc / ".managed_settings_keys.json",
)

# ── Steps 2-4: nested .gemini dir, GEMINI.md symlink, .env ──
dotgemini = xcode_gemini_dotgemini_dir()
dotgemini.mkdir(parents=True, exist_ok=True) # step 2
_ensure_gemini_md_symlink(dotgemini) # step 3
_sync_xcode_env_file(dotgemini, cfg) # step 4
Loading