Skip to content

Commit cf0babf

Browse files
committed
Refine profile handling in prime config
1 parent 8dfec09 commit cf0babf

8 files changed

Lines changed: 440 additions & 40 deletions

File tree

packages/prime/src/prime_cli/commands/config.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616

1717
# Team ID validation pattern: CUID (v1)
1818
TEAM_ID_PATTERN = re.compile(r"^c[a-z0-9]{24}$")
19+
PROFILE_OVERRIDE_ENV_VARS = (
20+
"PRIME_API_KEY",
21+
"PRIME_TEAM_ID",
22+
"PRIME_USER_ID",
23+
"PRIME_API_BASE_URL",
24+
"PRIME_BASE_URL",
25+
"PRIME_FRONTEND_URL",
26+
"PRIME_INFERENCE_URL",
27+
"PRIME_CONTEXT",
28+
)
1929

2030

2131
def validate_team_id(team_id: str) -> bool:
@@ -32,6 +42,34 @@ def validate_team_id(team_id: str) -> bool:
3242
return bool(TEAM_ID_PATTERN.match(team_id))
3343

3444

45+
def _active_profile_override_env_vars() -> list[str]:
46+
return [name for name in PROFILE_OVERRIDE_ENV_VARS if (os.getenv(name) or "").strip()]
47+
48+
49+
def _require_profile_env_unset(command: str) -> None:
50+
names = _active_profile_override_env_vars()
51+
if not names:
52+
return
53+
joined = ", ".join(names)
54+
console.print(
55+
f"[red]Error:[/red] {joined} {'is' if len(names) == 1 else 'are'} set in your "
56+
f"environment, so [bold]prime config {command}[/bold] cannot make a saved profile "
57+
"active. Unset the environment override and rerun the command."
58+
)
59+
raise typer.Exit(1)
60+
61+
62+
def _require_no_context_override(command: str) -> None:
63+
context = Config.context_override()
64+
if not context:
65+
return
66+
console.print(
67+
f"[red]Error:[/red] [bold]prime config {command}[/bold] cannot update auth while "
68+
f"PRIME_CONTEXT is set to '{context}'. Run the command without a temporary context."
69+
)
70+
raise typer.Exit(1)
71+
72+
3573
@app.command()
3674
def view() -> None:
3775
"""View current configuration"""
@@ -116,6 +154,8 @@ def set_api_key(
116154
),
117155
) -> None:
118156
"""Set your API key (prompts securely if not provided)"""
157+
_require_no_context_override("set-api-key")
158+
119159
if api_key is None:
120160
# Interactive mode with secure prompt
121161
api_key = typer.prompt(
@@ -126,6 +166,7 @@ def set_api_key(
126166
)
127167

128168
config = Config()
169+
config.use_production_environment()
129170
config.set_api_key(api_key)
130171

131172
if api_key:
@@ -140,7 +181,6 @@ def set_api_key(
140181
user_id = data.get("id")
141182
if user_id:
142183
config.set_user_id(user_id)
143-
config.update_current_environment_file()
144184
except (APIError, Exception):
145185
pass
146186

@@ -161,6 +201,7 @@ def set_team_id(
161201
),
162202
) -> None:
163203
"""Set your team ID."""
204+
_require_no_context_override("set-team-id")
164205
config = Config()
165206

166207
# Validate team ID format
@@ -186,6 +227,7 @@ def set_team_id(
186227
except (APIError, Exception):
187228
pass
188229

230+
config.use_production_environment()
189231
config.set_team(team_id, team_name=team_name, team_role=team_role)
190232
if team_id:
191233
if team_name:
@@ -199,7 +241,9 @@ def set_team_id(
199241
@app.command()
200242
def remove_team_id() -> None:
201243
"""Remove team ID to use personal account"""
244+
_require_no_context_override("remove-team-id")
202245
config = Config()
246+
config.use_production_environment()
203247
config.set_team(None)
204248
console.print("[green]Team ID removed. Using personal account.[/green]")
205249

@@ -278,6 +322,7 @@ def _set_environment(
278322
env: str,
279323
) -> None:
280324
"""Set URLs for a specific environment"""
325+
_require_profile_env_unset(f"use {env}")
281326
config = Config()
282327

283328
# Try to load the environment (handles both built-in and custom)
@@ -384,7 +429,7 @@ def reset(
384429
config.set_base_url(Config.DEFAULT_BASE_URL)
385430
config.set_frontend_url(Config.DEFAULT_FRONTEND_URL)
386431
config.set_ssh_key_path(Config.DEFAULT_SSH_KEY_PATH)
387-
config.set_current_environment("production")
432+
config.use_production_environment()
388433
console.print("[green]Configuration reset to defaults![/green]")
389434

390435

packages/prime/src/prime_cli/commands/login.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,25 @@
2020
console = get_console()
2121

2222

23+
def _require_no_context_override() -> None:
24+
context = Config.context_override()
25+
if not context:
26+
return
27+
console.print(
28+
f"[red]Error:[/red] [bold]prime login[/bold] cannot update auth while "
29+
f"PRIME_CONTEXT is set to '{context}'. Run the command without a temporary context."
30+
)
31+
raise typer.Exit(1)
32+
33+
2334
def fetch_and_select_team(client: APIClient, config: Config) -> None:
2435
"""Fetch user's teams and prompt for selection."""
2536
try:
2637
teams = fetch_teams(client)
2738

2839
if not teams:
40+
config.use_production_environment()
2941
config.set_team(None)
30-
config.update_current_environment_file()
3142
return
3243

3344
console.print("\n[bold]Select:[/bold]\n")
@@ -49,8 +60,8 @@ def fetch_and_select_team(client: APIClient, config: Config) -> None:
4960
selection = typer.prompt("Select", type=int, default=1)
5061

5162
if selection == 1:
63+
config.use_production_environment()
5264
config.set_team(None)
53-
config.update_current_environment_file()
5465
console.print("[green]Using personal account.[/green]")
5566
return
5667

@@ -62,27 +73,27 @@ def fetch_and_select_team(client: APIClient, config: Config) -> None:
6273

6374
if not team_id:
6475
console.print("[yellow]Invalid team. Using personal account.[/yellow]")
76+
config.use_production_environment()
6577
config.set_team(None)
66-
config.update_current_environment_file()
6778
return
6879

80+
config.use_production_environment()
6981
config.set_team(team_id, team_name=team_name, team_role=team_role)
70-
config.update_current_environment_file()
7182
console.print(f"[green]Using team '{team_name}'.[/green]")
7283
return
7384

7485
console.print(f"[red]Invalid selection. Enter 1-{len(teams) + 1}.[/red]")
7586
except Abort:
87+
config.use_production_environment()
7688
config.set_team(None)
77-
config.update_current_environment_file()
7889
return
7990

8091
except Abort:
92+
config.use_production_environment()
8193
config.set_team(None)
82-
config.update_current_environment_file()
8394
except (APIError, Exception):
95+
config.use_production_environment()
8496
config.set_team(None)
85-
config.update_current_environment_file()
8697

8798

8899
def generate_ephemeral_keypair() -> tuple[rsa.RSAPrivateKey, str]:
@@ -127,6 +138,7 @@ def login(
127138
headless: bool = typer.Option(False, "--headless", help="Don't attempt to open browser"),
128139
) -> None:
129140
"""Login to Prime Intellect"""
141+
_require_no_context_override()
130142
config = Config()
131143
settings = config.view()
132144

@@ -204,9 +216,8 @@ def login(
204216
if decrypted_result:
205217
# Update config with decrypted token
206218
api_key = decrypted_result.decode()
219+
config.use_production_environment()
207220
config.set_api_key(api_key)
208-
# Also update the current environment's saved file
209-
config.update_current_environment_file()
210221

211222
# Attempt to fetch the current user id
212223
client = APIClient(api_key=api_key)
@@ -219,7 +230,6 @@ def login(
219230
user_id = data.get("id")
220231
if user_id:
221232
config.set_user_id(user_id)
222-
config.update_current_environment_file()
223233
except (APIError, Exception):
224234
console.print("[yellow]Logged in, but failed to fetch user id[/yellow]")
225235

packages/prime/src/prime_cli/commands/switch.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
PERSONAL_TARGET = "personal"
1919

2020

21+
def _require_no_context_override() -> None:
22+
context = Config.context_override()
23+
if not context:
24+
return
25+
console.print(
26+
f"[red]Error:[/red] [bold]prime switch[/bold] cannot update auth while "
27+
f"PRIME_CONTEXT is set to '{context}'. Run the command without a temporary context."
28+
)
29+
raise typer.Exit(1)
30+
31+
2132
def _select_team_by_target(teams: list[dict], target: str) -> Optional[dict]:
2233
normalized_target = target.strip().lower()
2334

@@ -35,8 +46,8 @@ def _select_team_by_target(teams: list[dict], target: str) -> Optional[dict]:
3546

3647

3748
def _switch_to_personal(config: Config) -> None:
49+
config.use_production_environment()
3850
config.set_team(None)
39-
config.update_current_environment_file()
4051
console.print("[green]Switched to personal account.[/green]")
4152

4253

@@ -49,8 +60,8 @@ def _switch_to_team(config: Config, team: dict) -> None:
4960
console.print("[red]Error:[/red] Selected team is missing a team ID.")
5061
raise typer.Exit(1)
5162

63+
config.use_production_environment()
5264
config.set_team(team_id, team_name=team_name, team_role=team_role)
53-
config.update_current_environment_file()
5465
console.print(f"[green]Switched to team '{team_name}'.[/green]")
5566

5667

@@ -67,6 +78,7 @@ def switch(
6778
),
6879
) -> None:
6980
"""Switch the active account context."""
81+
_require_no_context_override()
7082
config = Config()
7183

7284
if config.team_id_from_env:

packages/prime/src/prime_cli/commands/whoami.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ def whoami() -> None:
3131

3232
# Update config
3333
config = Config()
34-
if user_id:
34+
if user_id and not Config.context_override():
35+
config.use_production_environment()
3536
config.set_user_id(user_id)
36-
config.update_current_environment_file()
3737

3838
# Display account info table
3939
table = Table(title="Account")

packages/prime/src/prime_cli/core/config.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def __init__(self) -> None:
4141
if context:
4242
self.load_environment(context, persist=False)
4343

44+
@staticmethod
45+
def context_override() -> str | None:
46+
context = os.getenv("PRIME_CONTEXT")
47+
if context and context.strip():
48+
return context.strip()
49+
return None
50+
4451
@staticmethod
4552
def _strip_api_v1(url: str) -> str:
4653
# make base_url consistent even if user passed a /api/v1 variant
@@ -214,6 +221,15 @@ def set_current_environment(self, value: str) -> None:
214221
self.config["current_environment"] = value
215222
self._save_config(self.config)
216223

224+
def use_production_environment(self) -> None:
225+
"""Use the built-in production endpoint configuration."""
226+
if self.current_environment != "production":
227+
self.config["base_url"] = self.DEFAULT_BASE_URL
228+
self.config["frontend_url"] = self.DEFAULT_FRONTEND_URL
229+
self.config["inference_url"] = self.DEFAULT_INFERENCE_URL
230+
self.config["current_environment"] = "production"
231+
self._save_config(self.config)
232+
217233
def _sanitize_environment_name(self, name: str) -> str:
218234
"""Sanitize environment name to prevent path traversal"""
219235
# Only allow alphanumeric characters, hyphens, and underscores
@@ -354,29 +370,6 @@ def load_environment(self, name: str, persist: bool = True) -> bool:
354370
raise
355371
return False
356372

357-
def update_current_environment_file(self) -> None:
358-
"""Update the current environment's saved file with current config"""
359-
if self.current_environment != "production":
360-
# Only update custom environments, not the built-in production
361-
try:
362-
sanitized_name = self._sanitize_environment_name(self.current_environment)
363-
env_file = self.environments_dir / f"{sanitized_name}.json"
364-
if env_file.exists():
365-
env_config = {
366-
"api_key": self.api_key,
367-
"team_id": self.team_id,
368-
"team_name": None if self.team_id_from_env else self.team_name,
369-
"team_role": None if self.team_id_from_env else self.team_role,
370-
"user_id": self.user_id,
371-
"base_url": self.base_url,
372-
"frontend_url": self.frontend_url,
373-
"inference_url": self.inference_url,
374-
}
375-
env_file.write_text(json.dumps(env_config, indent=2))
376-
except ValueError:
377-
# Skip updating if environment name is invalid
378-
pass
379-
380373
def list_environments(self) -> list[str]:
381374
"""List all saved environment names"""
382375
environments = ["production"] # Built-in environment

packages/prime/src/prime_cli/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,17 @@ def callback(
107107
typer.echo(f" - {env_name}", err=True)
108108
raise typer.Exit(1)
109109

110-
# Set environment variable so Config instances in subcommands pick it up
110+
previous_context = os.environ.get("PRIME_CONTEXT")
111+
112+
def restore_context() -> None:
113+
if previous_context is None:
114+
os.environ.pop("PRIME_CONTEXT", None)
115+
else:
116+
os.environ["PRIME_CONTEXT"] = previous_context
117+
118+
ctx.call_on_close(restore_context)
119+
120+
# Set environment variable so Config instances in subcommands pick it up.
111121
os.environ["PRIME_CONTEXT"] = context
112122

113123
# Check for updates (only when a subcommand is being executed)

packages/prime/tests/test_config_delete.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,32 @@
1111
"COLUMNS": "200",
1212
"LINES": "50",
1313
"PRIME_DISABLE_VERSION_CHECK": "1",
14+
"PRIME_API_KEY": "",
15+
"PRIME_TEAM_ID": "",
16+
"PRIME_USER_ID": "",
17+
"PRIME_API_BASE_URL": "",
18+
"PRIME_BASE_URL": "",
19+
"PRIME_FRONTEND_URL": "",
20+
"PRIME_INFERENCE_URL": "",
21+
"PRIME_CONTEXT": "",
1422
}
1523

1624

1725
@pytest.fixture
1826
def temp_home(tmp_path: Any, monkeypatch: pytest.MonkeyPatch) -> Path:
1927
monkeypatch.setenv("HOME", str(tmp_path))
2028
monkeypatch.setattr("prime_cli.main.check_for_update", lambda: (False, None))
29+
for name in (
30+
"PRIME_API_KEY",
31+
"PRIME_TEAM_ID",
32+
"PRIME_USER_ID",
33+
"PRIME_API_BASE_URL",
34+
"PRIME_BASE_URL",
35+
"PRIME_FRONTEND_URL",
36+
"PRIME_INFERENCE_URL",
37+
"PRIME_CONTEXT",
38+
):
39+
monkeypatch.delenv(name, raising=False)
2140
return tmp_path
2241

2342

0 commit comments

Comments
 (0)