ai-code-switcher 0.1.5__tar.gz → 0.1.7__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.
Files changed (20) hide show
  1. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/PKG-INFO +1 -1
  2. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/pyproject.toml +1 -1
  3. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/PKG-INFO +1 -1
  4. ai_code_switcher-0.1.7/src/code_ai/__init__.py +1 -0
  5. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/launcher.py +15 -2
  6. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/models.py +2 -2
  7. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/profiles.py +23 -13
  8. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/tests/test_integration.py +91 -0
  9. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/tests/test_launcher.py +33 -0
  10. ai_code_switcher-0.1.5/src/code_ai/__init__.py +0 -1
  11. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/README.md +0 -0
  12. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/setup.cfg +0 -0
  13. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/SOURCES.txt +0 -0
  14. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/dependency_links.txt +0 -0
  15. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/entry_points.txt +0 -0
  16. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/requires.txt +0 -0
  17. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/top_level.txt +0 -0
  18. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/cli.py +0 -0
  19. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/config.py +0 -0
  20. {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-code-switcher
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Switch AI coding tool profiles and launch the correct CLI
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.8
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-code-switcher"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "Switch AI coding tool profiles and launch the correct CLI"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-code-switcher
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Switch AI coding tool profiles and launch the correct CLI
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.8
@@ -0,0 +1 @@
1
+ __version__ = "0.1.6"
@@ -32,12 +32,25 @@ def prepare_environment(profile):
32
32
 
33
33
  # Handle authentication based on profile type
34
34
  if isinstance(profile, LoginProfile):
35
- # Login mode: clear API environment variables and set CLAUDE_CONFIG_DIR
35
+ # Login mode: clear API environment variables and set CONFIG_DIR
36
36
  for env_var in spec["env"]:
37
37
  env.pop(env_var, None)
38
38
  # Expand ~ to home directory
39
39
  credentials_path = os.path.expanduser(profile.credentials_path)
40
- env["CLAUDE_CONFIG_DIR"] = credentials_path
40
+ # Set the appropriate config dir env var based on profile type
41
+ # Claude uses CLAUDE_CONFIG_DIR, Codex uses CODEX_HOME
42
+ config_dir_vars = {"claude": "CLAUDE_CONFIG_DIR", "codex": "CODEX_HOME"}
43
+ config_dir_var = config_dir_vars.get(ptype)
44
+ if config_dir_var:
45
+ os.makedirs(credentials_path, exist_ok=True)
46
+ # For codex: ensure config.toml exists with default openai provider
47
+ # to prevent inheriting custom providers from ~/.codex/config.toml
48
+ if ptype == "codex":
49
+ config_toml = os.path.join(credentials_path, "config.toml")
50
+ if not os.path.exists(config_toml):
51
+ with open(config_toml, "w") as f:
52
+ f.write('model_provider = "openai"\n')
53
+ env[config_dir_var] = credentials_path
41
54
  elif isinstance(profile, ApiProfile):
42
55
  # API mode: set API environment variables
43
56
  for env_var, config_key in spec["env"].items():
@@ -22,7 +22,7 @@ class ApiProfile(BaseProfile):
22
22
 
23
23
  @dataclass
24
24
  class LoginProfile(BaseProfile):
25
- """Login mode: authenticate via OAuth credentials directory (Claude only)"""
25
+ """Login mode: authenticate via OAuth credentials directory (Claude/Codex)"""
26
26
  credentials_path: str = "" # Path to existing OAuth credentials
27
27
 
28
28
 
@@ -33,7 +33,7 @@ def profile_from_dict(data: dict) -> BaseProfile:
33
33
  mode = data.get("mode", "api") # Default to api for backward compatibility
34
34
  proxy = data.get("proxy")
35
35
 
36
- if ptype == "claude" and mode == "login":
36
+ if (ptype == "claude" or ptype == "codex") and mode == "login":
37
37
  return LoginProfile(
38
38
  name=name,
39
39
  type=ptype,
@@ -66,8 +66,8 @@ def add_profile(config):
66
66
  "type": ptype,
67
67
  }
68
68
 
69
- # Handle mode for Claude
70
- if ptype == "claude":
69
+ # Handle mode for Claude and Codex
70
+ if ptype in ("claude", "codex"):
71
71
  mode = input("Mode (api/login) [api]: ").strip().lower() or "api"
72
72
  if mode not in ("api", "login"):
73
73
  print("Error: mode must be 'api' or 'login'.")
@@ -77,17 +77,22 @@ def add_profile(config):
77
77
  if mode == "login":
78
78
  credentials_path = input("Credentials path (optional, auto-generate if empty): ").strip()
79
79
  if not credentials_path:
80
- credentials_path = f"~/.claude-profiles/{name}"
80
+ credentials_path = f"~/.{ptype}-profiles/{name}"
81
81
  profile_data["credentials_path"] = credentials_path
82
82
  else: # api mode
83
83
  base_url = input("Base URL: ").strip()
84
84
  if not base_url:
85
85
  print("Error: base URL cannot be empty.")
86
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
87
+ if ptype == "claude":
88
+ token = input("Auth token: ").strip()
89
+ profile_data["base_url"] = base_url
90
+ profile_data["token"] = token
91
+ else: # codex
92
+ api_key = input("API key: ").strip()
93
+ profile_data["base_url"] = base_url
94
+ profile_data["api_key"] = api_key
95
+ else: # gemini - API mode only
91
96
  base_url = input("Base URL: ").strip()
92
97
  if not base_url:
93
98
  print("Error: base URL cannot be empty.")
@@ -117,8 +122,8 @@ def show_profile(config, name):
117
122
  print(f"Profile: {name}")
118
123
  print(f"Type: {profile.type}")
119
124
 
120
- # Display mode for Claude type
121
- if profile.type == "claude":
125
+ # Display mode for Claude and Codex types
126
+ if profile.type in ("claude", "codex"):
122
127
  mode = profile_dict.get("mode", "api")
123
128
  print(f"Mode: {mode}")
124
129
 
@@ -127,10 +132,15 @@ def show_profile(config, name):
127
132
  print(f"Credentials Path: {credentials_path}")
128
133
  else: # api mode
129
134
  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
135
+ if profile.type == "claude":
136
+ token = profile_dict.get("token", "N/A")
137
+ print(f"Base URL: {base_url}")
138
+ print(f"Token: {token}")
139
+ else: # codex
140
+ api_key = profile_dict.get("api_key", "N/A")
141
+ print(f"Base URL: {base_url}")
142
+ print(f"API Key: {api_key}")
143
+ else: # gemini
134
144
  base_url = profile_dict.get("base_url", "N/A")
135
145
  api_key = profile_dict.get("api_key", "N/A")
136
146
  print(f"Base URL: {base_url}")
@@ -218,3 +218,94 @@ class TestBackwardCompatibility:
218
218
  env = prepare_environment(profile)
219
219
  assert env["GOOGLE_GEMINI_BASE_URL"] == "https://generativelanguage.googleapis.com"
220
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
@@ -72,3 +72,36 @@ def test_prepare_env_login_clears_api_vars():
72
72
  # Should be cleared
73
73
  assert "ANTHROPIC_BASE_URL" not in env
74
74
  assert "ANTHROPIC_AUTH_TOKEN" not in env
75
+
76
+
77
+ def test_prepare_env_codex_api_mode():
78
+ """Test environment preparation for codex API mode"""
79
+ profile = ApiProfile(
80
+ name="test-codex-api",
81
+ type="codex",
82
+ base_url="https://api.openai.com/v1",
83
+ api_key="sk-test"
84
+ )
85
+
86
+ env = prepare_environment(profile)
87
+
88
+ # Should have both OPENAI_API_KEY and OPENAI_BASE_URL
89
+ assert env["OPENAI_API_KEY"] == "sk-test"
90
+ assert env["OPENAI_BASE_URL"] == "https://api.openai.com/v1"
91
+
92
+
93
+ def test_prepare_env_codex_login_mode():
94
+ """Test environment preparation for codex login mode"""
95
+ profile = LoginProfile(
96
+ name="test-codex-login",
97
+ type="codex",
98
+ credentials_path="~/.codex-profiles/account-a"
99
+ )
100
+
101
+ env = prepare_environment(profile)
102
+
103
+ # Should NOT have API environment variables
104
+ assert "OPENAI_API_KEY" not in env
105
+ # Should have CODEX_HOME with expanded path
106
+ expected_path = os.path.expanduser("~/.codex-profiles/account-a")
107
+ assert env["CODEX_HOME"] == expected_path
@@ -1 +0,0 @@
1
- __version__ = "0.1.5"