ai-code-switcher 0.1.4__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/PKG-INFO +1 -1
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/pyproject.toml +1 -1
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/PKG-INFO +1 -1
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/SOURCES.txt +5 -1
- ai_code_switcher-0.1.5/src/code_ai/__init__.py +1 -0
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/code_ai/cli.py +10 -3
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/code_ai/config.py +10 -0
- ai_code_switcher-0.1.5/src/code_ai/launcher.py +93 -0
- ai_code_switcher-0.1.5/src/code_ai/models.py +66 -0
- ai_code_switcher-0.1.5/src/code_ai/profiles.py +150 -0
- ai_code_switcher-0.1.5/tests/test_integration.py +220 -0
- ai_code_switcher-0.1.5/tests/test_launcher.py +74 -0
- ai_code_switcher-0.1.5/tests/test_models.py +115 -0
- ai_code_switcher-0.1.4/src/code_ai/__init__.py +0 -1
- ai_code_switcher-0.1.4/src/code_ai/launcher.py +0 -58
- ai_code_switcher-0.1.4/src/code_ai/profiles.py +0 -80
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/README.md +0 -0
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/setup.cfg +0 -0
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/dependency_links.txt +0 -0
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/entry_points.txt +0 -0
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/requires.txt +0 -0
- {ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.5"
|
|
@@ -110,9 +110,16 @@ UPGRADE_PACKAGES = [
|
|
|
110
110
|
|
|
111
111
|
def upgrade():
|
|
112
112
|
typer.echo("Upgrading claude, codex, gemini CLI...")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
|
|
114
|
+
# On Windows, npm is a .cmd file, need to use shell=True or npm.cmd
|
|
115
|
+
if sys.platform == "win32":
|
|
116
|
+
# Use shell=True on Windows for better compatibility
|
|
117
|
+
cmd = "npm install -g " + " ".join(UPGRADE_PACKAGES)
|
|
118
|
+
result = subprocess.run(cmd, shell=True)
|
|
119
|
+
else:
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
["npm", "install", "-g"] + UPGRADE_PACKAGES,
|
|
122
|
+
)
|
|
116
123
|
sys.exit(result.returncode)
|
|
117
124
|
|
|
118
125
|
|
|
@@ -4,6 +4,8 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import yaml
|
|
6
6
|
|
|
7
|
+
from .models import profile_from_dict, profile_to_dict, BaseProfile
|
|
8
|
+
|
|
7
9
|
CONFIG_DIR = Path.home() / ".code-ai"
|
|
8
10
|
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
9
11
|
|
|
@@ -26,3 +28,11 @@ def save_config(data):
|
|
|
26
28
|
|
|
27
29
|
def init_config():
|
|
28
30
|
save_config({"profiles": {}})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_profile_object(config, name: str) -> BaseProfile:
|
|
34
|
+
"""Get a profile object from config by name."""
|
|
35
|
+
if name not in config.get("profiles", {}):
|
|
36
|
+
raise ValueError(f"Profile '{name}' not found in config")
|
|
37
|
+
profile_dict = config["profiles"][name]
|
|
38
|
+
return profile_from_dict(profile_dict)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from .models import profile_from_dict, ApiProfile, LoginProfile
|
|
6
|
+
|
|
7
|
+
ENV_MAP = {
|
|
8
|
+
"claude": {
|
|
9
|
+
"env": {"ANTHROPIC_BASE_URL": "base_url", "ANTHROPIC_AUTH_TOKEN": "token"},
|
|
10
|
+
"cmd": "claude",
|
|
11
|
+
},
|
|
12
|
+
"gemini": {
|
|
13
|
+
"env": {"GOOGLE_GEMINI_BASE_URL": "base_url", "GEMINI_API_KEY": "api_key"},
|
|
14
|
+
"cmd": "gemini",
|
|
15
|
+
},
|
|
16
|
+
"codex": {
|
|
17
|
+
"env": {"OPENAI_BASE_URL": "base_url", "OPENAI_API_KEY": "api_key"},
|
|
18
|
+
"cmd": "codex",
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prepare_environment(profile):
|
|
24
|
+
"""Prepare environment variables based on profile type and mode"""
|
|
25
|
+
env = os.environ.copy()
|
|
26
|
+
ptype = profile.type
|
|
27
|
+
|
|
28
|
+
if ptype not in ENV_MAP:
|
|
29
|
+
raise ValueError(f"Unknown profile type '{ptype}'")
|
|
30
|
+
|
|
31
|
+
spec = ENV_MAP[ptype]
|
|
32
|
+
|
|
33
|
+
# Handle authentication based on profile type
|
|
34
|
+
if isinstance(profile, LoginProfile):
|
|
35
|
+
# Login mode: clear API environment variables and set CLAUDE_CONFIG_DIR
|
|
36
|
+
for env_var in spec["env"]:
|
|
37
|
+
env.pop(env_var, None)
|
|
38
|
+
# Expand ~ to home directory
|
|
39
|
+
credentials_path = os.path.expanduser(profile.credentials_path)
|
|
40
|
+
env["CLAUDE_CONFIG_DIR"] = credentials_path
|
|
41
|
+
elif isinstance(profile, ApiProfile):
|
|
42
|
+
# API mode: set API environment variables
|
|
43
|
+
for env_var, config_key in spec["env"].items():
|
|
44
|
+
value = getattr(profile, config_key, None)
|
|
45
|
+
if value:
|
|
46
|
+
env[env_var] = value
|
|
47
|
+
|
|
48
|
+
# Handle proxy (all modes)
|
|
49
|
+
if profile.proxy:
|
|
50
|
+
env["HTTP_PROXY"] = profile.proxy
|
|
51
|
+
env["HTTPS_PROXY"] = profile.proxy
|
|
52
|
+
|
|
53
|
+
return env
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def launch(profile_dict, extra_args):
|
|
57
|
+
# Convert dict to dataclass
|
|
58
|
+
profile = profile_from_dict(profile_dict)
|
|
59
|
+
ptype = profile.type
|
|
60
|
+
|
|
61
|
+
if ptype not in ENV_MAP:
|
|
62
|
+
print(f"Error: unknown profile type '{ptype}'.")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
spec = ENV_MAP[ptype]
|
|
66
|
+
cmd = spec["cmd"]
|
|
67
|
+
|
|
68
|
+
# On Windows, npm global commands are .cmd files
|
|
69
|
+
if sys.platform == "win32":
|
|
70
|
+
cmd_path = shutil.which(f"{cmd}.cmd") or shutil.which(cmd)
|
|
71
|
+
else:
|
|
72
|
+
cmd_path = shutil.which(cmd)
|
|
73
|
+
|
|
74
|
+
if not cmd_path:
|
|
75
|
+
print(f"Error: '{cmd}' not found in PATH. Install it first.")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
# Prepare environment
|
|
79
|
+
env = prepare_environment(profile)
|
|
80
|
+
|
|
81
|
+
full_cmd = [cmd_path] + extra_args
|
|
82
|
+
|
|
83
|
+
if sys.platform == "win32":
|
|
84
|
+
# On Windows, use subprocess with full environment
|
|
85
|
+
try:
|
|
86
|
+
result = subprocess.run(full_cmd, env=env, shell=False)
|
|
87
|
+
sys.exit(result.returncode)
|
|
88
|
+
except KeyboardInterrupt:
|
|
89
|
+
sys.exit(130)
|
|
90
|
+
else:
|
|
91
|
+
# On Unix, update os.environ and use execvp
|
|
92
|
+
os.environ.update(env)
|
|
93
|
+
os.execvp(cmd, [cmd] + extra_args)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from dataclasses import dataclass, asdict, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
VALID_TYPES = ("claude", "gemini", "codex")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class BaseProfile:
|
|
9
|
+
"""Common fields for all profiles"""
|
|
10
|
+
name: str
|
|
11
|
+
type: str # "claude" | "gemini" | "codex"
|
|
12
|
+
proxy: Optional[str] = None # e.g., "http://127.0.0.1:7890"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ApiProfile(BaseProfile):
|
|
17
|
+
"""API mode: authenticate via base_url + token/api_key"""
|
|
18
|
+
base_url: str = ""
|
|
19
|
+
token: Optional[str] = None # Claude only
|
|
20
|
+
api_key: Optional[str] = None # Gemini/Codex only
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LoginProfile(BaseProfile):
|
|
25
|
+
"""Login mode: authenticate via OAuth credentials directory (Claude only)"""
|
|
26
|
+
credentials_path: str = "" # Path to existing OAuth credentials
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def profile_from_dict(data: dict) -> BaseProfile:
|
|
30
|
+
"""Convert dict to appropriate profile dataclass"""
|
|
31
|
+
name = data.get("name", "")
|
|
32
|
+
ptype = data.get("type", "")
|
|
33
|
+
mode = data.get("mode", "api") # Default to api for backward compatibility
|
|
34
|
+
proxy = data.get("proxy")
|
|
35
|
+
|
|
36
|
+
if ptype == "claude" and mode == "login":
|
|
37
|
+
return LoginProfile(
|
|
38
|
+
name=name,
|
|
39
|
+
type=ptype,
|
|
40
|
+
credentials_path=data.get("credentials_path", ""),
|
|
41
|
+
proxy=proxy
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
# API mode (default for all types)
|
|
45
|
+
return ApiProfile(
|
|
46
|
+
name=name,
|
|
47
|
+
type=ptype,
|
|
48
|
+
base_url=data.get("base_url", ""),
|
|
49
|
+
token=data.get("token"),
|
|
50
|
+
api_key=data.get("api_key"),
|
|
51
|
+
proxy=proxy
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def profile_to_dict(profile: BaseProfile) -> dict:
|
|
56
|
+
"""Convert profile dataclass to dict"""
|
|
57
|
+
data = asdict(profile)
|
|
58
|
+
|
|
59
|
+
# Add mode field
|
|
60
|
+
if isinstance(profile, LoginProfile):
|
|
61
|
+
data["mode"] = "login"
|
|
62
|
+
else:
|
|
63
|
+
data["mode"] = "api"
|
|
64
|
+
|
|
65
|
+
# Remove None values for cleaner YAML
|
|
66
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from .models import VALID_TYPES, profile_from_dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_profiles(config):
|
|
7
|
+
from .config import save_config
|
|
8
|
+
|
|
9
|
+
profiles = config.get("profiles", {})
|
|
10
|
+
|
|
11
|
+
# Auto-migrate old profiles without mode field
|
|
12
|
+
migrated = False
|
|
13
|
+
for name, p in profiles.items():
|
|
14
|
+
if "mode" not in p:
|
|
15
|
+
p["mode"] = "api"
|
|
16
|
+
migrated = True
|
|
17
|
+
|
|
18
|
+
if migrated:
|
|
19
|
+
save_config(config)
|
|
20
|
+
|
|
21
|
+
if not profiles:
|
|
22
|
+
print("No profiles configured.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
print(f"{'Name':<20} {'Type':<10} {'Mode':<8} {'Base URL/Credentials':<42} {'Proxy':<20}")
|
|
26
|
+
print("-" * 100)
|
|
27
|
+
|
|
28
|
+
for name, p in profiles.items():
|
|
29
|
+
profile = profile_from_dict(p)
|
|
30
|
+
ptype = profile.type
|
|
31
|
+
mode = p.get("mode", "api")
|
|
32
|
+
|
|
33
|
+
# Determine what to display in the Base URL/Credentials column
|
|
34
|
+
if mode == "login":
|
|
35
|
+
url_or_creds = p.get("credentials_path", "")
|
|
36
|
+
else: # api mode
|
|
37
|
+
url_or_creds = p.get("base_url", "")
|
|
38
|
+
|
|
39
|
+
# Truncate long strings
|
|
40
|
+
if len(url_or_creds) > 40:
|
|
41
|
+
url_or_creds = url_or_creds[:37] + "..."
|
|
42
|
+
|
|
43
|
+
# Get proxy or display "-"
|
|
44
|
+
proxy_display = profile.proxy if profile.proxy else "-"
|
|
45
|
+
if len(proxy_display) > 20:
|
|
46
|
+
proxy_display = proxy_display[:17] + "..."
|
|
47
|
+
|
|
48
|
+
print(f"{name:<20} {ptype:<10} {mode:<8} {url_or_creds:<42} {proxy_display:<20}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def add_profile(config):
|
|
52
|
+
name = input("Profile name: ").strip()
|
|
53
|
+
if not name:
|
|
54
|
+
print("Error: name cannot be empty.")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
if name in config.get("profiles", {}):
|
|
57
|
+
print(f"Error: profile '{name}' already exists.")
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
ptype = input(f"Type ({'/'.join(VALID_TYPES)}): ").strip().lower()
|
|
61
|
+
if ptype not in VALID_TYPES:
|
|
62
|
+
print(f"Error: type must be one of {VALID_TYPES}.")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
profile_data = {
|
|
66
|
+
"type": ptype,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Handle mode for Claude
|
|
70
|
+
if ptype == "claude":
|
|
71
|
+
mode = input("Mode (api/login) [api]: ").strip().lower() or "api"
|
|
72
|
+
if mode not in ("api", "login"):
|
|
73
|
+
print("Error: mode must be 'api' or 'login'.")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
profile_data["mode"] = mode
|
|
76
|
+
|
|
77
|
+
if mode == "login":
|
|
78
|
+
credentials_path = input("Credentials path (optional, auto-generate if empty): ").strip()
|
|
79
|
+
if not credentials_path:
|
|
80
|
+
credentials_path = f"~/.claude-profiles/{name}"
|
|
81
|
+
profile_data["credentials_path"] = credentials_path
|
|
82
|
+
else: # api mode
|
|
83
|
+
base_url = input("Base URL: ").strip()
|
|
84
|
+
if not base_url:
|
|
85
|
+
print("Error: base URL cannot be empty.")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
token = input("Auth token: ").strip()
|
|
88
|
+
profile_data["base_url"] = base_url
|
|
89
|
+
profile_data["token"] = token
|
|
90
|
+
else: # gemini or codex - API mode only
|
|
91
|
+
base_url = input("Base URL: ").strip()
|
|
92
|
+
if not base_url:
|
|
93
|
+
print("Error: base URL cannot be empty.")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
api_key = input("API key: ").strip()
|
|
96
|
+
profile_data["base_url"] = base_url
|
|
97
|
+
profile_data["api_key"] = api_key
|
|
98
|
+
|
|
99
|
+
# Optional proxy for all types
|
|
100
|
+
proxy = input("Proxy (optional, e.g., http://127.0.0.1:7890): ").strip()
|
|
101
|
+
if proxy:
|
|
102
|
+
profile_data["proxy"] = proxy
|
|
103
|
+
|
|
104
|
+
config.setdefault("profiles", {})[name] = profile_data
|
|
105
|
+
return config
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def show_profile(config, name):
|
|
109
|
+
profiles = config.get("profiles", {})
|
|
110
|
+
if name not in profiles:
|
|
111
|
+
print(f"Error: profile '{name}' not found.")
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
profile_dict = profiles[name]
|
|
115
|
+
profile = profile_from_dict(profile_dict)
|
|
116
|
+
|
|
117
|
+
print(f"Profile: {name}")
|
|
118
|
+
print(f"Type: {profile.type}")
|
|
119
|
+
|
|
120
|
+
# Display mode for Claude type
|
|
121
|
+
if profile.type == "claude":
|
|
122
|
+
mode = profile_dict.get("mode", "api")
|
|
123
|
+
print(f"Mode: {mode}")
|
|
124
|
+
|
|
125
|
+
if mode == "login":
|
|
126
|
+
credentials_path = profile_dict.get("credentials_path", "N/A")
|
|
127
|
+
print(f"Credentials Path: {credentials_path}")
|
|
128
|
+
else: # api mode
|
|
129
|
+
base_url = profile_dict.get("base_url", "N/A")
|
|
130
|
+
token = profile_dict.get("token", "N/A")
|
|
131
|
+
print(f"Base URL: {base_url}")
|
|
132
|
+
print(f"Token: {token}")
|
|
133
|
+
else: # gemini or codex
|
|
134
|
+
base_url = profile_dict.get("base_url", "N/A")
|
|
135
|
+
api_key = profile_dict.get("api_key", "N/A")
|
|
136
|
+
print(f"Base URL: {base_url}")
|
|
137
|
+
print(f"API Key: {api_key}")
|
|
138
|
+
|
|
139
|
+
# Display proxy if set
|
|
140
|
+
if profile.proxy:
|
|
141
|
+
print(f"Proxy: {profile.proxy}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def remove_profile(config, name):
|
|
145
|
+
if name not in config.get("profiles", {}):
|
|
146
|
+
print(f"Error: profile '{name}' not found.")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
del config["profiles"][name]
|
|
149
|
+
print(f"Removed profile '{name}'.")
|
|
150
|
+
return config
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from src.code_ai.config import load_config, save_config
|
|
4
|
+
from src.code_ai.profiles import add_profile, list_profiles, show_profile, remove_profile
|
|
5
|
+
from src.code_ai.models import profile_from_dict, ApiProfile, LoginProfile
|
|
6
|
+
from src.code_ai.launcher import prepare_environment
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestFullWorkflowApiProfile:
|
|
10
|
+
"""Test complete workflow for API profile (Claude)"""
|
|
11
|
+
|
|
12
|
+
def test_full_workflow_api_profile(self, monkeypatch, tmp_path):
|
|
13
|
+
"""Test full workflow: add API profile -> show -> list -> remove"""
|
|
14
|
+
# Mock config file location
|
|
15
|
+
config_file = tmp_path / "config.yaml"
|
|
16
|
+
|
|
17
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
18
|
+
# Step 1: Initialize config
|
|
19
|
+
config = {"profiles": {}}
|
|
20
|
+
save_config(config)
|
|
21
|
+
|
|
22
|
+
# Step 2: Add API profile interactively
|
|
23
|
+
inputs = [
|
|
24
|
+
"my-claude-api", # Profile name
|
|
25
|
+
"claude", # Type
|
|
26
|
+
"api", # Mode
|
|
27
|
+
"https://api.anthropic.com", # Base URL
|
|
28
|
+
"sk-ant-test-token", # Auth token
|
|
29
|
+
"", # No proxy
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
with patch("builtins.input", side_effect=inputs):
|
|
33
|
+
config = load_config()
|
|
34
|
+
config = add_profile(config)
|
|
35
|
+
save_config(config)
|
|
36
|
+
|
|
37
|
+
# Step 3: Verify profile was added
|
|
38
|
+
config = load_config()
|
|
39
|
+
assert "my-claude-api" in config["profiles"]
|
|
40
|
+
profile_dict = config["profiles"]["my-claude-api"]
|
|
41
|
+
assert profile_dict["type"] == "claude"
|
|
42
|
+
assert profile_dict["mode"] == "api"
|
|
43
|
+
assert profile_dict["base_url"] == "https://api.anthropic.com"
|
|
44
|
+
assert profile_dict["token"] == "sk-ant-test-token"
|
|
45
|
+
|
|
46
|
+
# Step 4: Convert to profile object and verify
|
|
47
|
+
profile = profile_from_dict(profile_dict)
|
|
48
|
+
assert isinstance(profile, ApiProfile)
|
|
49
|
+
assert profile.type == "claude"
|
|
50
|
+
|
|
51
|
+
# Step 5: Test environment preparation
|
|
52
|
+
env = prepare_environment(profile)
|
|
53
|
+
assert env["ANTHROPIC_BASE_URL"] == "https://api.anthropic.com"
|
|
54
|
+
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-test-token"
|
|
55
|
+
|
|
56
|
+
# Step 6: Show profile
|
|
57
|
+
with patch("builtins.print") as mock_print:
|
|
58
|
+
show_profile(config, "my-claude-api")
|
|
59
|
+
# Verify show_profile was called and printed something
|
|
60
|
+
assert mock_print.called
|
|
61
|
+
|
|
62
|
+
# Step 7: List profiles
|
|
63
|
+
with patch("builtins.print") as mock_print:
|
|
64
|
+
list_profiles(config)
|
|
65
|
+
# Verify list_profiles was called and printed something
|
|
66
|
+
assert mock_print.called
|
|
67
|
+
|
|
68
|
+
# Step 8: Remove profile
|
|
69
|
+
config = remove_profile(config, "my-claude-api")
|
|
70
|
+
assert "my-claude-api" not in config["profiles"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestFullWorkflowLoginProfile:
|
|
74
|
+
"""Test complete workflow for login profile (Claude)"""
|
|
75
|
+
|
|
76
|
+
def test_full_workflow_login_profile(self, monkeypatch, tmp_path):
|
|
77
|
+
"""Test full workflow: add login profile -> show -> list -> remove"""
|
|
78
|
+
# Mock config file location
|
|
79
|
+
config_file = tmp_path / "config.yaml"
|
|
80
|
+
|
|
81
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
82
|
+
# Step 1: Initialize config
|
|
83
|
+
config = {"profiles": {}}
|
|
84
|
+
save_config(config)
|
|
85
|
+
|
|
86
|
+
# Step 2: Add login profile interactively
|
|
87
|
+
inputs = [
|
|
88
|
+
"my-claude-login", # Profile name
|
|
89
|
+
"claude", # Type
|
|
90
|
+
"login", # Mode
|
|
91
|
+
"~/.claude-profiles/account-a", # Credentials path
|
|
92
|
+
"http://127.0.0.1:7890", # Proxy
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
with patch("builtins.input", side_effect=inputs):
|
|
96
|
+
config = load_config()
|
|
97
|
+
config = add_profile(config)
|
|
98
|
+
save_config(config)
|
|
99
|
+
|
|
100
|
+
# Step 3: Verify profile was added
|
|
101
|
+
config = load_config()
|
|
102
|
+
assert "my-claude-login" in config["profiles"]
|
|
103
|
+
profile_dict = config["profiles"]["my-claude-login"]
|
|
104
|
+
assert profile_dict["type"] == "claude"
|
|
105
|
+
assert profile_dict["mode"] == "login"
|
|
106
|
+
assert profile_dict["credentials_path"] == "~/.claude-profiles/account-a"
|
|
107
|
+
assert profile_dict["proxy"] == "http://127.0.0.1:7890"
|
|
108
|
+
|
|
109
|
+
# Step 4: Convert to profile object and verify
|
|
110
|
+
profile = profile_from_dict(profile_dict)
|
|
111
|
+
assert isinstance(profile, LoginProfile)
|
|
112
|
+
assert profile.type == "claude"
|
|
113
|
+
|
|
114
|
+
# Step 5: Test environment preparation
|
|
115
|
+
env = prepare_environment(profile)
|
|
116
|
+
# Login mode should NOT have API environment variables
|
|
117
|
+
assert "ANTHROPIC_BASE_URL" not in env
|
|
118
|
+
assert "ANTHROPIC_AUTH_TOKEN" not in env
|
|
119
|
+
# Should have CLAUDE_CONFIG_DIR with expanded path
|
|
120
|
+
import os
|
|
121
|
+
expected_path = os.path.expanduser("~/.claude-profiles/account-a")
|
|
122
|
+
assert env["CLAUDE_CONFIG_DIR"] == expected_path
|
|
123
|
+
# Should have proxy
|
|
124
|
+
assert env["HTTP_PROXY"] == "http://127.0.0.1:7890"
|
|
125
|
+
assert env["HTTPS_PROXY"] == "http://127.0.0.1:7890"
|
|
126
|
+
|
|
127
|
+
# Step 6: Show profile
|
|
128
|
+
with patch("builtins.print") as mock_print:
|
|
129
|
+
show_profile(config, "my-claude-login")
|
|
130
|
+
# Verify show_profile was called and printed something
|
|
131
|
+
assert mock_print.called
|
|
132
|
+
|
|
133
|
+
# Step 7: List profiles
|
|
134
|
+
with patch("builtins.print") as mock_print:
|
|
135
|
+
list_profiles(config)
|
|
136
|
+
# Verify list_profiles was called and printed something
|
|
137
|
+
assert mock_print.called
|
|
138
|
+
|
|
139
|
+
# Step 8: Remove profile
|
|
140
|
+
config = remove_profile(config, "my-claude-login")
|
|
141
|
+
assert "my-claude-login" not in config["profiles"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestBackwardCompatibility:
|
|
145
|
+
"""Test backward compatibility with legacy profiles"""
|
|
146
|
+
|
|
147
|
+
def test_backward_compatibility(self, tmp_path):
|
|
148
|
+
"""Test that legacy profiles (without mode field) still work"""
|
|
149
|
+
# Mock config file location
|
|
150
|
+
config_file = tmp_path / "config.yaml"
|
|
151
|
+
|
|
152
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
153
|
+
# Step 1: Create a legacy profile (without mode field)
|
|
154
|
+
legacy_config = {
|
|
155
|
+
"profiles": {
|
|
156
|
+
"legacy-claude": {
|
|
157
|
+
"name": "legacy-claude",
|
|
158
|
+
"type": "claude",
|
|
159
|
+
"base_url": "https://api.anthropic.com",
|
|
160
|
+
"token": "sk-ant-legacy-token"
|
|
161
|
+
# Note: no "mode" field
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
save_config(legacy_config)
|
|
166
|
+
|
|
167
|
+
# Step 2: Load config and verify it works
|
|
168
|
+
config = load_config()
|
|
169
|
+
assert "legacy-claude" in config["profiles"]
|
|
170
|
+
profile_dict = config["profiles"]["legacy-claude"]
|
|
171
|
+
|
|
172
|
+
# Step 3: Convert to profile object
|
|
173
|
+
# Should default to API mode
|
|
174
|
+
profile = profile_from_dict(profile_dict)
|
|
175
|
+
assert isinstance(profile, ApiProfile)
|
|
176
|
+
assert profile.base_url == "https://api.anthropic.com"
|
|
177
|
+
assert profile.token == "sk-ant-legacy-token"
|
|
178
|
+
|
|
179
|
+
# Step 4: Test environment preparation
|
|
180
|
+
env = prepare_environment(profile)
|
|
181
|
+
assert env["ANTHROPIC_BASE_URL"] == "https://api.anthropic.com"
|
|
182
|
+
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-legacy-token"
|
|
183
|
+
|
|
184
|
+
# Step 5: Show profile should work
|
|
185
|
+
with patch("builtins.print") as mock_print:
|
|
186
|
+
show_profile(config, "legacy-claude")
|
|
187
|
+
assert mock_print.called
|
|
188
|
+
|
|
189
|
+
# Step 6: List profiles should work
|
|
190
|
+
with patch("builtins.print") as mock_print:
|
|
191
|
+
list_profiles(config)
|
|
192
|
+
assert mock_print.called
|
|
193
|
+
|
|
194
|
+
# Step 7: Test with other profile types (gemini, codex)
|
|
195
|
+
gemini_config = {
|
|
196
|
+
"profiles": {
|
|
197
|
+
"legacy-gemini": {
|
|
198
|
+
"name": "legacy-gemini",
|
|
199
|
+
"type": "gemini",
|
|
200
|
+
"base_url": "https://generativelanguage.googleapis.com",
|
|
201
|
+
"api_key": "AIza-test-key"
|
|
202
|
+
# Note: no "mode" field
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
save_config(gemini_config)
|
|
207
|
+
|
|
208
|
+
config = load_config()
|
|
209
|
+
profile_dict = config["profiles"]["legacy-gemini"]
|
|
210
|
+
profile = profile_from_dict(profile_dict)
|
|
211
|
+
|
|
212
|
+
# Should default to API mode
|
|
213
|
+
assert isinstance(profile, ApiProfile)
|
|
214
|
+
assert profile.type == "gemini"
|
|
215
|
+
assert profile.api_key == "AIza-test-key"
|
|
216
|
+
|
|
217
|
+
# Test environment preparation
|
|
218
|
+
env = prepare_environment(profile)
|
|
219
|
+
assert env["GOOGLE_GEMINI_BASE_URL"] == "https://generativelanguage.googleapis.com"
|
|
220
|
+
assert env["GEMINI_API_KEY"] == "AIza-test-key"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from src.code_ai.models import ApiProfile, LoginProfile
|
|
5
|
+
from src.code_ai.launcher import prepare_environment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_prepare_env_api_mode():
|
|
9
|
+
"""Test environment preparation for API mode"""
|
|
10
|
+
profile = ApiProfile(
|
|
11
|
+
name="test-api",
|
|
12
|
+
type="claude",
|
|
13
|
+
base_url="https://api.anthropic.com",
|
|
14
|
+
token="sk-ant-test"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
env = prepare_environment(profile)
|
|
18
|
+
|
|
19
|
+
assert env["ANTHROPIC_BASE_URL"] == "https://api.anthropic.com"
|
|
20
|
+
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-test"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_prepare_env_login_mode():
|
|
24
|
+
"""Test environment preparation for login mode"""
|
|
25
|
+
profile = LoginProfile(
|
|
26
|
+
name="test-login",
|
|
27
|
+
type="claude",
|
|
28
|
+
credentials_path="~/.claude-profiles/account-a"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
env = prepare_environment(profile)
|
|
32
|
+
|
|
33
|
+
# Should NOT have API environment variables
|
|
34
|
+
assert "ANTHROPIC_BASE_URL" not in env
|
|
35
|
+
assert "ANTHROPIC_AUTH_TOKEN" not in env
|
|
36
|
+
# Should have CLAUDE_CONFIG_DIR with expanded path
|
|
37
|
+
expected_path = os.path.expanduser("~/.claude-profiles/account-a")
|
|
38
|
+
assert env["CLAUDE_CONFIG_DIR"] == expected_path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_prepare_env_with_proxy():
|
|
42
|
+
"""Test environment preparation with proxy"""
|
|
43
|
+
profile = ApiProfile(
|
|
44
|
+
name="test-proxy",
|
|
45
|
+
type="gemini",
|
|
46
|
+
base_url="https://generativelanguage.googleapis.com",
|
|
47
|
+
api_key="AIza-test",
|
|
48
|
+
proxy="http://127.0.0.1:7890"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
env = prepare_environment(profile)
|
|
52
|
+
|
|
53
|
+
assert env["HTTP_PROXY"] == "http://127.0.0.1:7890"
|
|
54
|
+
assert env["HTTPS_PROXY"] == "http://127.0.0.1:7890"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_prepare_env_login_clears_api_vars():
|
|
58
|
+
"""Test that login mode clears API environment variables"""
|
|
59
|
+
# Set up environment with existing API variables
|
|
60
|
+
with patch.dict(os.environ, {
|
|
61
|
+
"ANTHROPIC_BASE_URL": "https://old.url",
|
|
62
|
+
"ANTHROPIC_AUTH_TOKEN": "old-token"
|
|
63
|
+
}):
|
|
64
|
+
profile = LoginProfile(
|
|
65
|
+
name="test-login",
|
|
66
|
+
type="claude",
|
|
67
|
+
credentials_path="~/.claude-profiles/account-a"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
env = prepare_environment(profile)
|
|
71
|
+
|
|
72
|
+
# Should be cleared
|
|
73
|
+
assert "ANTHROPIC_BASE_URL" not in env
|
|
74
|
+
assert "ANTHROPIC_AUTH_TOKEN" not in env
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from src.code_ai.models import (
|
|
3
|
+
ApiProfile, LoginProfile, profile_from_dict, profile_to_dict, VALID_TYPES
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
def test_api_profile_creation():
|
|
7
|
+
"""Test creating an API profile"""
|
|
8
|
+
profile = ApiProfile(
|
|
9
|
+
name="test-api",
|
|
10
|
+
type="claude",
|
|
11
|
+
base_url="https://api.anthropic.com",
|
|
12
|
+
token="sk-ant-test"
|
|
13
|
+
)
|
|
14
|
+
assert profile.name == "test-api"
|
|
15
|
+
assert profile.type == "claude"
|
|
16
|
+
assert profile.base_url == "https://api.anthropic.com"
|
|
17
|
+
assert profile.token == "sk-ant-test"
|
|
18
|
+
assert profile.proxy is None
|
|
19
|
+
|
|
20
|
+
def test_login_profile_creation():
|
|
21
|
+
"""Test creating a login profile"""
|
|
22
|
+
profile = LoginProfile(
|
|
23
|
+
name="test-login",
|
|
24
|
+
type="claude",
|
|
25
|
+
credentials_path="~/.claude-profiles/account-a"
|
|
26
|
+
)
|
|
27
|
+
assert profile.name == "test-login"
|
|
28
|
+
assert profile.type == "claude"
|
|
29
|
+
assert profile.credentials_path == "~/.claude-profiles/account-a"
|
|
30
|
+
assert profile.proxy is None
|
|
31
|
+
|
|
32
|
+
def test_profile_with_proxy():
|
|
33
|
+
"""Test profile with proxy"""
|
|
34
|
+
profile = ApiProfile(
|
|
35
|
+
name="test-proxy",
|
|
36
|
+
type="gemini",
|
|
37
|
+
base_url="https://generativelanguage.googleapis.com",
|
|
38
|
+
api_key="AIza-test",
|
|
39
|
+
proxy="http://127.0.0.1:7890"
|
|
40
|
+
)
|
|
41
|
+
assert profile.proxy == "http://127.0.0.1:7890"
|
|
42
|
+
|
|
43
|
+
def test_profile_from_dict_api_mode():
|
|
44
|
+
"""Test converting dict to API profile"""
|
|
45
|
+
data = {
|
|
46
|
+
"name": "my-api",
|
|
47
|
+
"type": "claude",
|
|
48
|
+
"mode": "api",
|
|
49
|
+
"base_url": "https://api.anthropic.com",
|
|
50
|
+
"token": "sk-ant-test",
|
|
51
|
+
"proxy": None
|
|
52
|
+
}
|
|
53
|
+
profile = profile_from_dict(data)
|
|
54
|
+
assert isinstance(profile, ApiProfile)
|
|
55
|
+
assert profile.name == "my-api"
|
|
56
|
+
assert profile.base_url == "https://api.anthropic.com"
|
|
57
|
+
|
|
58
|
+
def test_profile_from_dict_login_mode():
|
|
59
|
+
"""Test converting dict to login profile"""
|
|
60
|
+
data = {
|
|
61
|
+
"name": "my-login",
|
|
62
|
+
"type": "claude",
|
|
63
|
+
"mode": "login",
|
|
64
|
+
"credentials_path": "~/.claude-profiles/account-a",
|
|
65
|
+
"proxy": "http://127.0.0.1:7890"
|
|
66
|
+
}
|
|
67
|
+
profile = profile_from_dict(data)
|
|
68
|
+
assert isinstance(profile, LoginProfile)
|
|
69
|
+
assert profile.credentials_path == "~/.claude-profiles/account-a"
|
|
70
|
+
assert profile.proxy == "http://127.0.0.1:7890"
|
|
71
|
+
|
|
72
|
+
def test_profile_from_dict_defaults_to_api():
|
|
73
|
+
"""Test that missing mode defaults to api"""
|
|
74
|
+
data = {
|
|
75
|
+
"name": "legacy",
|
|
76
|
+
"type": "claude",
|
|
77
|
+
"base_url": "https://api.anthropic.com",
|
|
78
|
+
"token": "sk-ant-test"
|
|
79
|
+
}
|
|
80
|
+
profile = profile_from_dict(data)
|
|
81
|
+
assert isinstance(profile, ApiProfile)
|
|
82
|
+
|
|
83
|
+
def test_profile_to_dict_api():
|
|
84
|
+
"""Test converting API profile to dict"""
|
|
85
|
+
profile = ApiProfile(
|
|
86
|
+
name="my-api",
|
|
87
|
+
type="claude",
|
|
88
|
+
base_url="https://api.anthropic.com",
|
|
89
|
+
token="sk-ant-test"
|
|
90
|
+
)
|
|
91
|
+
data = profile_to_dict(profile)
|
|
92
|
+
assert data["name"] == "my-api"
|
|
93
|
+
assert data["type"] == "claude"
|
|
94
|
+
assert data["mode"] == "api"
|
|
95
|
+
assert data["base_url"] == "https://api.anthropic.com"
|
|
96
|
+
assert data["token"] == "sk-ant-test"
|
|
97
|
+
|
|
98
|
+
def test_profile_to_dict_login():
|
|
99
|
+
"""Test converting login profile to dict"""
|
|
100
|
+
profile = LoginProfile(
|
|
101
|
+
name="my-login",
|
|
102
|
+
type="claude",
|
|
103
|
+
credentials_path="~/.claude-profiles/account-a",
|
|
104
|
+
proxy="http://127.0.0.1:7890"
|
|
105
|
+
)
|
|
106
|
+
data = profile_to_dict(profile)
|
|
107
|
+
assert data["name"] == "my-login"
|
|
108
|
+
assert data["type"] == "claude"
|
|
109
|
+
assert data["mode"] == "login"
|
|
110
|
+
assert data["credentials_path"] == "~/.claude-profiles/account-a"
|
|
111
|
+
assert data["proxy"] == "http://127.0.0.1:7890"
|
|
112
|
+
|
|
113
|
+
def test_valid_types():
|
|
114
|
+
"""Test VALID_TYPES constant"""
|
|
115
|
+
assert VALID_TYPES == ("claude", "gemini", "codex")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.3"
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import shutil
|
|
4
|
-
import subprocess
|
|
5
|
-
|
|
6
|
-
ENV_MAP = {
|
|
7
|
-
"claude": {
|
|
8
|
-
"env": {"ANTHROPIC_BASE_URL": "base_url", "ANTHROPIC_AUTH_TOKEN": "token"},
|
|
9
|
-
"cmd": "claude",
|
|
10
|
-
},
|
|
11
|
-
"gemini": {
|
|
12
|
-
"env": {"GOOGLE_GEMINI_BASE_URL": "base_url", "GEMINI_API_KEY": "api_key"},
|
|
13
|
-
"cmd": "gemini",
|
|
14
|
-
},
|
|
15
|
-
"codex": {
|
|
16
|
-
"env": {"OPENAI_BASE_URL": "base_url", "OPENAI_API_KEY": "api_key"},
|
|
17
|
-
"cmd": "codex",
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def launch(profile, extra_args):
|
|
23
|
-
ptype = profile["type"]
|
|
24
|
-
if ptype not in ENV_MAP:
|
|
25
|
-
print(f"Error: unknown profile type '{ptype}'.")
|
|
26
|
-
sys.exit(1)
|
|
27
|
-
|
|
28
|
-
spec = ENV_MAP[ptype]
|
|
29
|
-
cmd = spec["cmd"]
|
|
30
|
-
|
|
31
|
-
# On Windows, npm global commands are .cmd files
|
|
32
|
-
if sys.platform == "win32":
|
|
33
|
-
cmd_path = shutil.which(f"{cmd}.cmd") or shutil.which(cmd)
|
|
34
|
-
else:
|
|
35
|
-
cmd_path = shutil.which(cmd)
|
|
36
|
-
|
|
37
|
-
if not cmd_path:
|
|
38
|
-
print(f"Error: '{cmd}' not found in PATH. Install it first.")
|
|
39
|
-
sys.exit(1)
|
|
40
|
-
|
|
41
|
-
env = os.environ.copy()
|
|
42
|
-
for env_var, config_key in spec["env"].items():
|
|
43
|
-
value = profile.get(config_key)
|
|
44
|
-
if value:
|
|
45
|
-
env[env_var] = value
|
|
46
|
-
|
|
47
|
-
full_cmd = [cmd_path] + extra_args
|
|
48
|
-
|
|
49
|
-
if sys.platform == "win32":
|
|
50
|
-
# On Windows, use subprocess with shell=False for better compatibility
|
|
51
|
-
try:
|
|
52
|
-
result = subprocess.run(full_cmd, env=env, shell=False)
|
|
53
|
-
sys.exit(result.returncode)
|
|
54
|
-
except KeyboardInterrupt:
|
|
55
|
-
sys.exit(130)
|
|
56
|
-
else:
|
|
57
|
-
os.environ.update(env)
|
|
58
|
-
os.execvp(cmd, [cmd] + extra_args)
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
VALID_TYPES = ("claude", "gemini", "codex")
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def list_profiles(config):
|
|
8
|
-
profiles = config.get("profiles", {})
|
|
9
|
-
if not profiles:
|
|
10
|
-
print("No profiles configured.")
|
|
11
|
-
return
|
|
12
|
-
print(f"{'Name':<20} {'Type':<10} {'Base URL'}")
|
|
13
|
-
print("-" * 60)
|
|
14
|
-
for name, p in profiles.items():
|
|
15
|
-
print(f"{name:<20} {p['type']:<10} {p.get('base_url', '')}")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def add_profile(config):
|
|
19
|
-
name = input("Profile name: ").strip()
|
|
20
|
-
if not name:
|
|
21
|
-
print("Error: name cannot be empty.")
|
|
22
|
-
sys.exit(1)
|
|
23
|
-
if name in config.get("profiles", {}):
|
|
24
|
-
print(f"Error: profile '{name}' already exists.")
|
|
25
|
-
sys.exit(1)
|
|
26
|
-
|
|
27
|
-
ptype = input(f"Type ({'/'.join(VALID_TYPES)}): ").strip().lower()
|
|
28
|
-
if ptype not in VALID_TYPES:
|
|
29
|
-
print(f"Error: type must be one of {VALID_TYPES}.")
|
|
30
|
-
sys.exit(1)
|
|
31
|
-
|
|
32
|
-
base_url = input("Base URL: ").strip()
|
|
33
|
-
if not base_url:
|
|
34
|
-
print("Error: base URL cannot be empty.")
|
|
35
|
-
sys.exit(1)
|
|
36
|
-
|
|
37
|
-
if ptype == "claude":
|
|
38
|
-
token = input("Auth token: ").strip()
|
|
39
|
-
config.setdefault("profiles", {})[name] = {
|
|
40
|
-
"type": ptype,
|
|
41
|
-
"base_url": base_url,
|
|
42
|
-
"token": token,
|
|
43
|
-
}
|
|
44
|
-
else:
|
|
45
|
-
api_key = input("API key: ").strip()
|
|
46
|
-
config.setdefault("profiles", {})[name] = {
|
|
47
|
-
"type": ptype,
|
|
48
|
-
"base_url": base_url,
|
|
49
|
-
"api_key": api_key,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return config
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def show_profile(config, name):
|
|
56
|
-
profiles = config.get("profiles", {})
|
|
57
|
-
if name not in profiles:
|
|
58
|
-
print(f"Error: profile '{name}' not found.")
|
|
59
|
-
sys.exit(1)
|
|
60
|
-
|
|
61
|
-
profile = profiles[name]
|
|
62
|
-
print(f"Profile: {name}")
|
|
63
|
-
print(f"Type: {profile['type']}")
|
|
64
|
-
print(f"Base URL: {profile.get('base_url', 'N/A')}")
|
|
65
|
-
|
|
66
|
-
if profile['type'] == 'claude':
|
|
67
|
-
token = profile.get('token', 'N/A')
|
|
68
|
-
print(f"Token: {token}")
|
|
69
|
-
else:
|
|
70
|
-
api_key = profile.get('api_key', 'N/A')
|
|
71
|
-
print(f"API Key: {api_key}")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def remove_profile(config, name):
|
|
75
|
-
if name not in config.get("profiles", {}):
|
|
76
|
-
print(f"Error: profile '{name}' not found.")
|
|
77
|
-
sys.exit(1)
|
|
78
|
-
del config["profiles"][name]
|
|
79
|
-
print(f"Removed profile '{name}'.")
|
|
80
|
-
return config
|
|
File without changes
|
|
File without changes
|
{ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/requires.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.4 → ai_code_switcher-0.1.5}/src/ai_code_switcher.egg-info/top_level.txt
RENAMED
|
File without changes
|