ai-code-switcher 0.1.7__tar.gz → 0.1.8__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.7 → ai_code_switcher-0.1.8}/PKG-INFO +1 -1
- ai_code_switcher-0.1.8/README.md +98 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/pyproject.toml +1 -1
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/PKG-INFO +1 -1
- ai_code_switcher-0.1.8/src/code_ai/__init__.py +1 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/code_ai/config.py +13 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/code_ai/launcher.py +31 -8
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/code_ai/profiles.py +1 -12
- ai_code_switcher-0.1.8/tests/test_integration.py +278 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/tests/test_launcher.py +27 -0
- ai_code_switcher-0.1.7/README.md +0 -98
- ai_code_switcher-0.1.7/src/code_ai/__init__.py +0 -1
- ai_code_switcher-0.1.7/tests/test_integration.py +0 -311
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/setup.cfg +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/SOURCES.txt +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/dependency_links.txt +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/entry_points.txt +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/requires.txt +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/top_level.txt +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/code_ai/cli.py +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/code_ai/models.py +0 -0
- {ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/tests/test_models.py +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ai-code-switcher
|
|
2
|
+
|
|
3
|
+
Switch AI coding tool profiles and launch the correct CLI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Manage multiple AI coding tool profiles for Claude, Codex, and Gemini
|
|
8
|
+
- Switch between API-mode and login-mode profiles
|
|
9
|
+
- Launch the matching CLI through one command entrypoint
|
|
10
|
+
- Upgrade supported AI CLIs through npm
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
List profiles:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
code-ai list
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Add a profile:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
code-ai add
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Show one profile:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
code-ai show <profile-name>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Launch a profile:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
code-ai run fox-gemini
|
|
42
|
+
code-ai run 4399
|
|
43
|
+
code-ai run fox-claude -p "hi"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Remove a profile:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
code-ai remove <profile-name>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Upgrade supported CLIs:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
code-ai upgrade
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This upgrades:
|
|
59
|
+
|
|
60
|
+
- `@anthropic-ai/claude-code`
|
|
61
|
+
- `@openai/codex`
|
|
62
|
+
- `@google/gemini-cli`
|
|
63
|
+
|
|
64
|
+
Version:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
code-ai --version
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Help:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
code-ai --help
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
Profiles are stored under `~/.code-ai/config.yaml`.
|
|
79
|
+
|
|
80
|
+
## Project Layout
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
src/code_ai/
|
|
84
|
+
|-- __init__.py
|
|
85
|
+
|-- cli.py
|
|
86
|
+
|-- config.py
|
|
87
|
+
|-- launcher.py
|
|
88
|
+
`-- profiles.py
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python >= 3.8
|
|
94
|
+
- pyyaml >= 5.0
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.8"
|
|
@@ -17,6 +17,19 @@ def load_config():
|
|
|
17
17
|
data = yaml.safe_load(f)
|
|
18
18
|
if not data or "profiles" not in data:
|
|
19
19
|
data = {"profiles": {}}
|
|
20
|
+
migrated = False
|
|
21
|
+
profiles = data.get("profiles", {})
|
|
22
|
+
for name, profile in profiles.items():
|
|
23
|
+
if not isinstance(profile, dict):
|
|
24
|
+
continue
|
|
25
|
+
if "name" not in profile:
|
|
26
|
+
profile["name"] = name
|
|
27
|
+
migrated = True
|
|
28
|
+
if "mode" not in profile:
|
|
29
|
+
profile["mode"] = "api"
|
|
30
|
+
migrated = True
|
|
31
|
+
if migrated:
|
|
32
|
+
save_config(data)
|
|
20
33
|
return data
|
|
21
34
|
|
|
22
35
|
|
|
@@ -19,10 +19,35 @@ ENV_MAP = {
|
|
|
19
19
|
},
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
CONFIG_DIR_ENV_VARS = {
|
|
23
|
+
"claude": "CLAUDE_CONFIG_DIR",
|
|
24
|
+
"codex": "CODEX_HOME",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
PROXY_ENV_VARS = (
|
|
28
|
+
"HTTP_PROXY",
|
|
29
|
+
"HTTPS_PROXY",
|
|
30
|
+
"http_proxy",
|
|
31
|
+
"https_proxy",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
MANAGED_ENV_VARS = frozenset(
|
|
35
|
+
env_var
|
|
36
|
+
for spec in ENV_MAP.values()
|
|
37
|
+
for env_var in spec["env"]
|
|
38
|
+
) | frozenset(CONFIG_DIR_ENV_VARS.values()) | frozenset(PROXY_ENV_VARS)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clear_managed_environment(env):
|
|
42
|
+
"""Remove environment variables managed by this tool."""
|
|
43
|
+
for env_var in MANAGED_ENV_VARS:
|
|
44
|
+
env.pop(env_var, None)
|
|
45
|
+
|
|
22
46
|
|
|
23
47
|
def prepare_environment(profile):
|
|
24
48
|
"""Prepare environment variables based on profile type and mode"""
|
|
25
49
|
env = os.environ.copy()
|
|
50
|
+
clear_managed_environment(env)
|
|
26
51
|
ptype = profile.type
|
|
27
52
|
|
|
28
53
|
if ptype not in ENV_MAP:
|
|
@@ -32,23 +57,21 @@ def prepare_environment(profile):
|
|
|
32
57
|
|
|
33
58
|
# Handle authentication based on profile type
|
|
34
59
|
if isinstance(profile, LoginProfile):
|
|
35
|
-
# Login mode: clear API environment variables and set CONFIG_DIR
|
|
36
|
-
for env_var in spec["env"]:
|
|
37
|
-
env.pop(env_var, None)
|
|
38
60
|
# Expand ~ to home directory
|
|
39
61
|
credentials_path = os.path.expanduser(profile.credentials_path)
|
|
40
62
|
# Set the appropriate config dir env var based on profile type
|
|
41
63
|
# Claude uses CLAUDE_CONFIG_DIR, Codex uses CODEX_HOME
|
|
42
|
-
|
|
43
|
-
config_dir_var = config_dir_vars.get(ptype)
|
|
64
|
+
config_dir_var = CONFIG_DIR_ENV_VARS.get(ptype)
|
|
44
65
|
if config_dir_var:
|
|
66
|
+
if not credentials_path:
|
|
67
|
+
raise ValueError(f"Login profile '{profile.name or ptype}' is missing credentials_path")
|
|
45
68
|
os.makedirs(credentials_path, exist_ok=True)
|
|
46
69
|
# For codex: ensure config.toml exists with default openai provider
|
|
47
70
|
# to prevent inheriting custom providers from ~/.codex/config.toml
|
|
48
71
|
if ptype == "codex":
|
|
49
72
|
config_toml = os.path.join(credentials_path, "config.toml")
|
|
50
73
|
if not os.path.exists(config_toml):
|
|
51
|
-
with open(config_toml, "w") as f:
|
|
74
|
+
with open(config_toml, "w", encoding="utf-8") as f:
|
|
52
75
|
f.write('model_provider = "openai"\n')
|
|
53
76
|
env[config_dir_var] = credentials_path
|
|
54
77
|
elif isinstance(profile, ApiProfile):
|
|
@@ -60,8 +83,8 @@ def prepare_environment(profile):
|
|
|
60
83
|
|
|
61
84
|
# Handle proxy (all modes)
|
|
62
85
|
if profile.proxy:
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
for env_var in PROXY_ENV_VARS:
|
|
87
|
+
env[env_var] = profile.proxy
|
|
65
88
|
|
|
66
89
|
return env
|
|
67
90
|
|
|
@@ -4,20 +4,8 @@ from .models import VALID_TYPES, profile_from_dict
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def list_profiles(config):
|
|
7
|
-
from .config import save_config
|
|
8
|
-
|
|
9
7
|
profiles = config.get("profiles", {})
|
|
10
8
|
|
|
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
9
|
if not profiles:
|
|
22
10
|
print("No profiles configured.")
|
|
23
11
|
return
|
|
@@ -63,6 +51,7 @@ def add_profile(config):
|
|
|
63
51
|
sys.exit(1)
|
|
64
52
|
|
|
65
53
|
profile_data = {
|
|
54
|
+
"name": name,
|
|
66
55
|
"type": ptype,
|
|
67
56
|
}
|
|
68
57
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shutil
|
|
4
|
+
import os
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from src.code_ai.config import load_config, save_config
|
|
9
|
+
from src.code_ai.profiles import add_profile, list_profiles, show_profile, remove_profile
|
|
10
|
+
from src.code_ai.models import profile_from_dict, ApiProfile, LoginProfile
|
|
11
|
+
from src.code_ai.launcher import prepare_environment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@contextmanager
|
|
15
|
+
def temp_config_file():
|
|
16
|
+
root = Path.cwd() / ".test-artifacts" / str(uuid4())
|
|
17
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
try:
|
|
19
|
+
yield root / "config.yaml"
|
|
20
|
+
finally:
|
|
21
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestFullWorkflowApiProfile:
|
|
25
|
+
"""Test complete workflow for API profile (Claude)"""
|
|
26
|
+
|
|
27
|
+
def test_full_workflow_api_profile(self):
|
|
28
|
+
"""Test full workflow: add API profile -> show -> list -> remove"""
|
|
29
|
+
with temp_config_file() as config_file:
|
|
30
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
31
|
+
config = {"profiles": {}}
|
|
32
|
+
save_config(config)
|
|
33
|
+
|
|
34
|
+
inputs = [
|
|
35
|
+
"my-claude-api",
|
|
36
|
+
"claude",
|
|
37
|
+
"api",
|
|
38
|
+
"https://api.anthropic.com",
|
|
39
|
+
"sk-ant-test-token",
|
|
40
|
+
"",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
with patch("builtins.input", side_effect=inputs):
|
|
44
|
+
config = load_config()
|
|
45
|
+
config = add_profile(config)
|
|
46
|
+
save_config(config)
|
|
47
|
+
|
|
48
|
+
config = load_config()
|
|
49
|
+
assert "my-claude-api" in config["profiles"]
|
|
50
|
+
profile_dict = config["profiles"]["my-claude-api"]
|
|
51
|
+
assert profile_dict["name"] == "my-claude-api"
|
|
52
|
+
assert profile_dict["type"] == "claude"
|
|
53
|
+
assert profile_dict["mode"] == "api"
|
|
54
|
+
assert profile_dict["base_url"] == "https://api.anthropic.com"
|
|
55
|
+
assert profile_dict["token"] == "sk-ant-test-token"
|
|
56
|
+
|
|
57
|
+
profile = profile_from_dict(profile_dict)
|
|
58
|
+
assert isinstance(profile, ApiProfile)
|
|
59
|
+
assert profile.name == "my-claude-api"
|
|
60
|
+
assert profile.type == "claude"
|
|
61
|
+
|
|
62
|
+
env = prepare_environment(profile)
|
|
63
|
+
assert env["ANTHROPIC_BASE_URL"] == "https://api.anthropic.com"
|
|
64
|
+
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-test-token"
|
|
65
|
+
|
|
66
|
+
with patch("builtins.print") as mock_print:
|
|
67
|
+
show_profile(config, "my-claude-api")
|
|
68
|
+
assert mock_print.called
|
|
69
|
+
|
|
70
|
+
with patch("builtins.print") as mock_print:
|
|
71
|
+
list_profiles(config)
|
|
72
|
+
assert mock_print.called
|
|
73
|
+
|
|
74
|
+
config = remove_profile(config, "my-claude-api")
|
|
75
|
+
assert "my-claude-api" not in config["profiles"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestFullWorkflowLoginProfile:
|
|
79
|
+
"""Test complete workflow for login profile (Claude)"""
|
|
80
|
+
|
|
81
|
+
def test_full_workflow_login_profile(self):
|
|
82
|
+
"""Test full workflow: add login profile -> show -> list -> remove"""
|
|
83
|
+
with temp_config_file() as config_file:
|
|
84
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
85
|
+
config = {"profiles": {}}
|
|
86
|
+
save_config(config)
|
|
87
|
+
|
|
88
|
+
inputs = [
|
|
89
|
+
"my-claude-login",
|
|
90
|
+
"claude",
|
|
91
|
+
"login",
|
|
92
|
+
"~/.claude-profiles/account-a",
|
|
93
|
+
"http://127.0.0.1:7890",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
with patch("builtins.input", side_effect=inputs):
|
|
97
|
+
config = load_config()
|
|
98
|
+
config = add_profile(config)
|
|
99
|
+
save_config(config)
|
|
100
|
+
|
|
101
|
+
config = load_config()
|
|
102
|
+
assert "my-claude-login" in config["profiles"]
|
|
103
|
+
profile_dict = config["profiles"]["my-claude-login"]
|
|
104
|
+
assert profile_dict["name"] == "my-claude-login"
|
|
105
|
+
assert profile_dict["type"] == "claude"
|
|
106
|
+
assert profile_dict["mode"] == "login"
|
|
107
|
+
assert profile_dict["credentials_path"] == "~/.claude-profiles/account-a"
|
|
108
|
+
assert profile_dict["proxy"] == "http://127.0.0.1:7890"
|
|
109
|
+
|
|
110
|
+
profile = profile_from_dict(profile_dict)
|
|
111
|
+
assert isinstance(profile, LoginProfile)
|
|
112
|
+
assert profile.name == "my-claude-login"
|
|
113
|
+
assert profile.type == "claude"
|
|
114
|
+
|
|
115
|
+
env = prepare_environment(profile)
|
|
116
|
+
assert "ANTHROPIC_BASE_URL" not in env
|
|
117
|
+
assert "ANTHROPIC_AUTH_TOKEN" not in env
|
|
118
|
+
assert env["CLAUDE_CONFIG_DIR"] == os.path.expanduser("~/.claude-profiles/account-a")
|
|
119
|
+
assert env["HTTP_PROXY"] == "http://127.0.0.1:7890"
|
|
120
|
+
assert env["HTTPS_PROXY"] == "http://127.0.0.1:7890"
|
|
121
|
+
|
|
122
|
+
with patch("builtins.print") as mock_print:
|
|
123
|
+
show_profile(config, "my-claude-login")
|
|
124
|
+
assert mock_print.called
|
|
125
|
+
|
|
126
|
+
with patch("builtins.print") as mock_print:
|
|
127
|
+
list_profiles(config)
|
|
128
|
+
assert mock_print.called
|
|
129
|
+
|
|
130
|
+
config = remove_profile(config, "my-claude-login")
|
|
131
|
+
assert "my-claude-login" not in config["profiles"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestBackwardCompatibility:
|
|
135
|
+
"""Test backward compatibility with legacy profiles"""
|
|
136
|
+
|
|
137
|
+
def test_backward_compatibility(self):
|
|
138
|
+
"""Test that legacy profiles without name/mode still work and are migrated"""
|
|
139
|
+
with temp_config_file() as config_file:
|
|
140
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
141
|
+
legacy_config = {
|
|
142
|
+
"profiles": {
|
|
143
|
+
"legacy-claude": {
|
|
144
|
+
"type": "claude",
|
|
145
|
+
"base_url": "https://api.anthropic.com",
|
|
146
|
+
"token": "sk-ant-legacy-token",
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
save_config(legacy_config)
|
|
151
|
+
|
|
152
|
+
config = load_config()
|
|
153
|
+
assert "legacy-claude" in config["profiles"]
|
|
154
|
+
profile_dict = config["profiles"]["legacy-claude"]
|
|
155
|
+
assert profile_dict["name"] == "legacy-claude"
|
|
156
|
+
assert profile_dict["mode"] == "api"
|
|
157
|
+
|
|
158
|
+
profile = profile_from_dict(profile_dict)
|
|
159
|
+
assert isinstance(profile, ApiProfile)
|
|
160
|
+
assert profile.name == "legacy-claude"
|
|
161
|
+
assert profile.base_url == "https://api.anthropic.com"
|
|
162
|
+
assert profile.token == "sk-ant-legacy-token"
|
|
163
|
+
|
|
164
|
+
env = prepare_environment(profile)
|
|
165
|
+
assert env["ANTHROPIC_BASE_URL"] == "https://api.anthropic.com"
|
|
166
|
+
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-legacy-token"
|
|
167
|
+
|
|
168
|
+
with patch("builtins.print") as mock_print:
|
|
169
|
+
show_profile(config, "legacy-claude")
|
|
170
|
+
assert mock_print.called
|
|
171
|
+
|
|
172
|
+
with patch("builtins.print") as mock_print:
|
|
173
|
+
list_profiles(config)
|
|
174
|
+
assert mock_print.called
|
|
175
|
+
|
|
176
|
+
gemini_config = {
|
|
177
|
+
"profiles": {
|
|
178
|
+
"legacy-gemini": {
|
|
179
|
+
"type": "gemini",
|
|
180
|
+
"base_url": "https://generativelanguage.googleapis.com",
|
|
181
|
+
"api_key": "AIza-test-key",
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
save_config(gemini_config)
|
|
186
|
+
|
|
187
|
+
config = load_config()
|
|
188
|
+
profile_dict = config["profiles"]["legacy-gemini"]
|
|
189
|
+
assert profile_dict["name"] == "legacy-gemini"
|
|
190
|
+
assert profile_dict["mode"] == "api"
|
|
191
|
+
|
|
192
|
+
profile = profile_from_dict(profile_dict)
|
|
193
|
+
assert isinstance(profile, ApiProfile)
|
|
194
|
+
assert profile.name == "legacy-gemini"
|
|
195
|
+
assert profile.type == "gemini"
|
|
196
|
+
assert profile.api_key == "AIza-test-key"
|
|
197
|
+
|
|
198
|
+
env = prepare_environment(profile)
|
|
199
|
+
assert env["GOOGLE_GEMINI_BASE_URL"] == "https://generativelanguage.googleapis.com"
|
|
200
|
+
assert env["GEMINI_API_KEY"] == "AIza-test-key"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestCodexProfiles:
|
|
204
|
+
"""Test codex profile workflows"""
|
|
205
|
+
|
|
206
|
+
def test_codex_api_profile(self):
|
|
207
|
+
"""Test codex API mode profile"""
|
|
208
|
+
with temp_config_file() as config_file:
|
|
209
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
210
|
+
config = {"profiles": {}}
|
|
211
|
+
save_config(config)
|
|
212
|
+
|
|
213
|
+
inputs = [
|
|
214
|
+
"my-codex-api",
|
|
215
|
+
"codex",
|
|
216
|
+
"api",
|
|
217
|
+
"https://api.openai.com/v1",
|
|
218
|
+
"sk-test-key",
|
|
219
|
+
"",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
with patch("builtins.input", side_effect=inputs):
|
|
223
|
+
config = load_config()
|
|
224
|
+
config = add_profile(config)
|
|
225
|
+
save_config(config)
|
|
226
|
+
|
|
227
|
+
config = load_config()
|
|
228
|
+
assert "my-codex-api" in config["profiles"]
|
|
229
|
+
profile_dict = config["profiles"]["my-codex-api"]
|
|
230
|
+
assert profile_dict["name"] == "my-codex-api"
|
|
231
|
+
assert profile_dict["type"] == "codex"
|
|
232
|
+
assert profile_dict["mode"] == "api"
|
|
233
|
+
|
|
234
|
+
profile = profile_from_dict(profile_dict)
|
|
235
|
+
assert isinstance(profile, ApiProfile)
|
|
236
|
+
assert profile.name == "my-codex-api"
|
|
237
|
+
assert profile.type == "codex"
|
|
238
|
+
|
|
239
|
+
env = prepare_environment(profile)
|
|
240
|
+
assert env["OPENAI_API_KEY"] == "sk-test-key"
|
|
241
|
+
assert env["OPENAI_BASE_URL"] == "https://api.openai.com/v1"
|
|
242
|
+
|
|
243
|
+
def test_codex_login_profile(self):
|
|
244
|
+
"""Test codex login mode profile"""
|
|
245
|
+
with temp_config_file() as config_file:
|
|
246
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
247
|
+
config = {"profiles": {}}
|
|
248
|
+
save_config(config)
|
|
249
|
+
|
|
250
|
+
inputs = [
|
|
251
|
+
"my-codex-login",
|
|
252
|
+
"codex",
|
|
253
|
+
"login",
|
|
254
|
+
"~/.codex-profiles/account-a",
|
|
255
|
+
"",
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
with patch("builtins.input", side_effect=inputs):
|
|
259
|
+
config = load_config()
|
|
260
|
+
config = add_profile(config)
|
|
261
|
+
save_config(config)
|
|
262
|
+
|
|
263
|
+
config = load_config()
|
|
264
|
+
assert "my-codex-login" in config["profiles"]
|
|
265
|
+
profile_dict = config["profiles"]["my-codex-login"]
|
|
266
|
+
assert profile_dict["name"] == "my-codex-login"
|
|
267
|
+
assert profile_dict["type"] == "codex"
|
|
268
|
+
assert profile_dict["mode"] == "login"
|
|
269
|
+
assert profile_dict["credentials_path"] == "~/.codex-profiles/account-a"
|
|
270
|
+
|
|
271
|
+
profile = profile_from_dict(profile_dict)
|
|
272
|
+
assert isinstance(profile, LoginProfile)
|
|
273
|
+
assert profile.name == "my-codex-login"
|
|
274
|
+
assert profile.type == "codex"
|
|
275
|
+
|
|
276
|
+
env = prepare_environment(profile)
|
|
277
|
+
assert "OPENAI_API_KEY" not in env
|
|
278
|
+
assert env["CODEX_HOME"] == os.path.expanduser("~/.codex-profiles/account-a")
|
|
@@ -20,6 +20,33 @@ def test_prepare_env_api_mode():
|
|
|
20
20
|
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-test"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def test_prepare_env_api_mode_clears_stale_managed_vars():
|
|
24
|
+
"""API mode should not inherit stale managed vars from the parent shell"""
|
|
25
|
+
with patch.dict(os.environ, {
|
|
26
|
+
"ANTHROPIC_BASE_URL": "https://old.url",
|
|
27
|
+
"ANTHROPIC_AUTH_TOKEN": "old-token",
|
|
28
|
+
"CLAUDE_CONFIG_DIR": "/tmp/old-claude",
|
|
29
|
+
"OPENAI_API_KEY": "old-openai-key",
|
|
30
|
+
"HTTP_PROXY": "http://127.0.0.1:9999",
|
|
31
|
+
"HTTPS_PROXY": "http://127.0.0.1:9999",
|
|
32
|
+
}):
|
|
33
|
+
profile = ApiProfile(
|
|
34
|
+
name="test-api",
|
|
35
|
+
type="claude",
|
|
36
|
+
base_url="https://api.anthropic.com",
|
|
37
|
+
token="sk-ant-test"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
env = prepare_environment(profile)
|
|
41
|
+
|
|
42
|
+
assert env["ANTHROPIC_BASE_URL"] == "https://api.anthropic.com"
|
|
43
|
+
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-ant-test"
|
|
44
|
+
assert "CLAUDE_CONFIG_DIR" not in env
|
|
45
|
+
assert "OPENAI_API_KEY" not in env
|
|
46
|
+
assert "HTTP_PROXY" not in env
|
|
47
|
+
assert "HTTPS_PROXY" not in env
|
|
48
|
+
|
|
49
|
+
|
|
23
50
|
def test_prepare_env_login_mode():
|
|
24
51
|
"""Test environment preparation for login mode"""
|
|
25
52
|
profile = LoginProfile(
|
ai_code_switcher-0.1.7/README.md
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# ai-code-switcher
|
|
2
|
-
|
|
3
|
-
一个用于切换 AI 编码工具配置文件并启动相应 CLI 的工具。
|
|
4
|
-
|
|
5
|
-
## 功能特性
|
|
6
|
-
|
|
7
|
-
- 管理多个 AI 编码工具配置文件(Claude、Codex、Gemini)
|
|
8
|
-
- 快速切换不同的配置文件
|
|
9
|
-
- 统一的命令行接口
|
|
10
|
-
- 支持一键升级所有 AI CLI 工具
|
|
11
|
-
|
|
12
|
-
## 安装
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
pip install -e .
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## 使用方法
|
|
19
|
-
|
|
20
|
-
### 列出所有配置文件
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
code-ai list
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### 添加新配置文件
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
code-ai add
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 使用指定配置文件启动
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
# 使用 fox-gemini 配置启动 Gemini CLI
|
|
36
|
-
code-ai fox-gemini
|
|
37
|
-
|
|
38
|
-
# 使用 4399 配置启动 Claude CLI
|
|
39
|
-
code-ai 4399
|
|
40
|
-
|
|
41
|
-
# 传递额外参数
|
|
42
|
-
code-ai fox-claude -p "hi"
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 删除配置文件
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
code-ai remove <profile-name>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### 升级 AI CLI 工具
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
code-ai upgrade
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
该命令会通过 npm 升级以下工具:
|
|
58
|
-
- @anthropic-ai/claude-code
|
|
59
|
-
- @openai/codex
|
|
60
|
-
- @google/gemini-cli
|
|
61
|
-
|
|
62
|
-
### 查看版本
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
code-ai --version
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 查看帮助
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
code-ai --help
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## 配置
|
|
75
|
-
|
|
76
|
-
配置文件存储在用户目录下,包含各个配置文件的设置信息。
|
|
77
|
-
|
|
78
|
-
## 开发
|
|
79
|
-
|
|
80
|
-
### 项目结构
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
src/code_ai/
|
|
84
|
-
├── __init__.py # 包初始化
|
|
85
|
-
├── cli.py # 命令行入口
|
|
86
|
-
├── config.py # 配置管理
|
|
87
|
-
├── launcher.py # 启动器
|
|
88
|
-
└── profiles.py # 配置文件管理
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### 依赖
|
|
92
|
-
|
|
93
|
-
- Python >= 3.8
|
|
94
|
-
- pyyaml >= 5.0
|
|
95
|
-
|
|
96
|
-
## 许可证
|
|
97
|
-
|
|
98
|
-
MIT
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.6"
|
|
@@ -1,311 +0,0 @@
|
|
|
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"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
class TestCodexProfiles:
|
|
224
|
-
"""Test codex profile workflows"""
|
|
225
|
-
|
|
226
|
-
def test_codex_api_profile(self, tmp_path):
|
|
227
|
-
"""Test codex API mode profile"""
|
|
228
|
-
config_file = tmp_path / "config.yaml"
|
|
229
|
-
|
|
230
|
-
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
231
|
-
# Initialize config
|
|
232
|
-
config = {"profiles": {}}
|
|
233
|
-
save_config(config)
|
|
234
|
-
|
|
235
|
-
# Add codex API profile
|
|
236
|
-
inputs = [
|
|
237
|
-
"my-codex-api", # Profile name
|
|
238
|
-
"codex", # Type
|
|
239
|
-
"api", # Mode
|
|
240
|
-
"https://api.openai.com/v1", # Base URL
|
|
241
|
-
"sk-test-key", # API key
|
|
242
|
-
"", # No proxy
|
|
243
|
-
]
|
|
244
|
-
|
|
245
|
-
with patch("builtins.input", side_effect=inputs):
|
|
246
|
-
config = load_config()
|
|
247
|
-
config = add_profile(config)
|
|
248
|
-
save_config(config)
|
|
249
|
-
|
|
250
|
-
# Verify profile was added
|
|
251
|
-
config = load_config()
|
|
252
|
-
assert "my-codex-api" in config["profiles"]
|
|
253
|
-
profile_dict = config["profiles"]["my-codex-api"]
|
|
254
|
-
assert profile_dict["type"] == "codex"
|
|
255
|
-
assert profile_dict["mode"] == "api"
|
|
256
|
-
|
|
257
|
-
# Convert to profile object
|
|
258
|
-
profile = profile_from_dict(profile_dict)
|
|
259
|
-
assert isinstance(profile, ApiProfile)
|
|
260
|
-
assert profile.type == "codex"
|
|
261
|
-
|
|
262
|
-
# Test environment preparation
|
|
263
|
-
env = prepare_environment(profile)
|
|
264
|
-
# Should have both OPENAI_API_KEY and OPENAI_BASE_URL
|
|
265
|
-
assert env["OPENAI_API_KEY"] == "sk-test-key"
|
|
266
|
-
assert env["OPENAI_BASE_URL"] == "https://api.openai.com/v1"
|
|
267
|
-
|
|
268
|
-
def test_codex_login_profile(self, tmp_path):
|
|
269
|
-
"""Test codex login mode profile"""
|
|
270
|
-
config_file = tmp_path / "config.yaml"
|
|
271
|
-
|
|
272
|
-
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
273
|
-
# Initialize config
|
|
274
|
-
config = {"profiles": {}}
|
|
275
|
-
save_config(config)
|
|
276
|
-
|
|
277
|
-
# Add codex login profile
|
|
278
|
-
inputs = [
|
|
279
|
-
"my-codex-login", # Profile name
|
|
280
|
-
"codex", # Type
|
|
281
|
-
"login", # Mode
|
|
282
|
-
"~/.codex-profiles/account-a", # Credentials path
|
|
283
|
-
"", # No proxy
|
|
284
|
-
]
|
|
285
|
-
|
|
286
|
-
with patch("builtins.input", side_effect=inputs):
|
|
287
|
-
config = load_config()
|
|
288
|
-
config = add_profile(config)
|
|
289
|
-
save_config(config)
|
|
290
|
-
|
|
291
|
-
# Verify profile was added
|
|
292
|
-
config = load_config()
|
|
293
|
-
assert "my-codex-login" in config["profiles"]
|
|
294
|
-
profile_dict = config["profiles"]["my-codex-login"]
|
|
295
|
-
assert profile_dict["type"] == "codex"
|
|
296
|
-
assert profile_dict["mode"] == "login"
|
|
297
|
-
assert profile_dict["credentials_path"] == "~/.codex-profiles/account-a"
|
|
298
|
-
|
|
299
|
-
# Convert to profile object
|
|
300
|
-
profile = profile_from_dict(profile_dict)
|
|
301
|
-
assert isinstance(profile, LoginProfile)
|
|
302
|
-
assert profile.type == "codex"
|
|
303
|
-
|
|
304
|
-
# Test environment preparation
|
|
305
|
-
env = prepare_environment(profile)
|
|
306
|
-
# Login mode should NOT have API environment variables
|
|
307
|
-
assert "OPENAI_API_KEY" not in env
|
|
308
|
-
# Should have CODEX_HOME with expanded path
|
|
309
|
-
import os
|
|
310
|
-
expected_path = os.path.expanduser("~/.codex-profiles/account-a")
|
|
311
|
-
assert env["CODEX_HOME"] == expected_path
|
|
File without changes
|
|
File without changes
|
{ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/requires.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.7 → ai_code_switcher-0.1.8}/src/ai_code_switcher.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|