Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions packages/prime/src/prime_cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def _env_set(*names: str) -> bool:
ssh_label += " (from env var)"
table.add_row("SSH Key Path", ssh_label)

# Show share resources with team
share_label = str(settings.get("share_resources_with_team", False))
table.add_row("Share Resources With Team", share_label)

console.print(table)


Expand Down Expand Up @@ -151,18 +155,13 @@ def set_api_key(

@app.command()
def set_team_id(
team_id: Optional[str] = typer.Argument(
None,
help="Your Prime Intellect team ID. Leave empty for personal account.",
team_id: str = typer.Argument(
...,
help="Your Prime Intellect team ID.",
),
) -> None:
"""Set your team ID. Empty team ID means personal account."""
if team_id is None:
# Interactive mode with prompt
team_id = typer.prompt(
"Enter your Prime Intellect team ID (leave empty for personal account)",
default="",
)
"""Set your team ID."""
config = Config()

# Validate team ID format
if not validate_team_id(team_id):
Expand All @@ -173,7 +172,6 @@ def set_team_id(
)
raise typer.Exit(code=1)

config = Config()
team_name = None
team_role = None
if team_id:
Expand Down Expand Up @@ -330,6 +328,24 @@ def _list_environments() -> None:
console.print(table)


@app.command(no_args_is_help=True)
def set_share_resources_with_team(
enabled: str = typer.Argument(
...,
help="Enable or disable auto-sharing with team: true or false",
),
) -> None:
"""Set whether to automatically share new resources with all team members"""
value = enabled.lower()
if value not in ("true", "false"):
console.print("[red]Error: Value must be 'true' or 'false'[/red]")
raise typer.Exit(1)

config = Config()
config.set_share_resources_with_team(value == "true")
console.print(f"[green]Share resources with team set to: {value}[/green]")


@app.command(no_args_is_help=True)
def set_ssh_key_path(
path: str = typer.Argument(
Expand Down
110 changes: 105 additions & 5 deletions packages/prime/src/prime_cli/commands/pods.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ..api.availability import AvailabilityClient, GPUAvailability
from ..api.pods import HistoryObj, Pod, PodsClient, PodStatus
from ..client import APIClient, APIError
from ..commands.teams import fetch_team_members
from ..helper.short_id import generate_short_id
from ..utils import (
confirm_or_skip,
Expand All @@ -30,7 +31,6 @@

app = typer.Typer(help="Manage compute pods", no_args_is_help=True)
console = Console()
config = Config()


def _format_pod_for_status(status: PodStatus, pod_details: Pod) -> Dict[str, Any]:
Expand Down Expand Up @@ -397,15 +397,44 @@ def create(
help="Environment variables to set in the pod. Can be specified multiple times "
"using --env KEY=value --env KEY2=value2",
),
share_with_team: bool = typer.Option(
False,
"--share-with-team",
help="Share the pod with all team members",
),
add_members: bool = typer.Option(
False,
"--add-members",
help="Interactively select team members to share the pod with",
),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
) -> None:
"""Create a new pod with an interactive setup process"""
config = Config()
env_vars = []
if env:
for env_var in env:
key, value = env_var.split("=")
env_vars.append({"key": key, "value": value})

# Resolve team_id
resolved_team_id = team_id or config.team_id

if share_with_team and add_members:
console.print(
"[red]Error: --share-with-team and --add-members are mutually exclusive. "
"Use only one of them.[/red]"
)
raise typer.Exit(1)

# Error if user explicitly passed sharing flags without a team
if (share_with_team or add_members) and not resolved_team_id:
console.print(
"[red]Error: --share-with-team and --add-members require a team. "
"Use --team-id or set a team with 'prime config set-team-id'[/red]"
)
raise typer.Exit(1)

try:
# Validate custom template usage
if custom_template_id and not image == "custom_template":
Expand Down Expand Up @@ -686,6 +715,70 @@ def select_provider_from_configs(

image = available_images[image_idx - 1]

# Determine sharing settings
shared_with_team = share_with_team
team_member_ids: List[str] = []
sharing_display: Optional[str] = None

if add_members and resolved_team_id:
# Fetch and display team members for interactive selection
members = fetch_team_members(base_client, resolved_team_id)

# Exclude the current user from the selection list
current_user_id = config.user_id
selectable_members = [m for m in members if m.get("userId") != current_user_id]

Comment thread
burnpiro marked this conversation as resolved.
if not selectable_members:
console.print("[yellow]No other team members to share with.[/yellow]")
else:
console.print("\n[bold]Team Members:[/bold]")
for idx, m in enumerate(selectable_members, 1):
member_name = m.get("userName") or "N/A"
email = m.get("userEmail") or "N/A"
role = m.get("role", "")
console.print(f" {idx}. {member_name} ({email}) - {role}")

selection = typer.prompt(
"\nSelect members (comma-separated numbers, or 'all' for everyone)",
default="all",
)

if selection.strip().lower() == "all":
shared_with_team = True
sharing_display = "All team members"
else:
selected_indices = []
for part in selection.split(","):
part = part.strip()
if part.isdigit():
idx = int(part)
if 1 <= idx <= len(selectable_members):
selected_indices.append(idx - 1)
else:
console.print(
f"[red]Invalid selection: {idx}. "
f"Must be between 1 and "
f"{len(selectable_members)}[/red]"
)
raise typer.Exit(1)
else:
console.print(f"[red]Invalid input: {part}[/red]")
raise typer.Exit(1)

selected_members = [selectable_members[i] for i in selected_indices]
team_member_ids = [m["userId"] for m in selected_members]
names = [m.get("userName") or m["userId"] for m in selected_members]
sharing_display = ", ".join(names)
Comment thread
cursor[bot] marked this conversation as resolved.

elif not share_with_team and not add_members:
# Check config default — only apply when a team is actually set
if resolved_team_id and config.share_resources_with_team:
shared_with_team = True
sharing_display = "All team members (from config default)"

if shared_with_team and not sharing_display:
sharing_display = "All team members"

# Create pod configuration
pod_config = {
"pod": {
Expand All @@ -710,12 +803,17 @@ def select_provider_from_configs(
"provider": {"type": selected_gpu.provider} if selected_gpu.provider else {},
"disks": disks,
"team": {
"teamId": team_id,
"teamId": resolved_team_id,
}
if team_id
if resolved_team_id
else None,
}

if shared_with_team:
pod_config["sharedWithTeam"] = True
if team_member_ids:
pod_config["teamMemberIds"] = team_member_ids

# Show configuration summary
console.print("\n[bold]Pod Configuration Summary:[/bold]")
pod_dict = pod_config.get("pod", {})
Expand All @@ -729,9 +827,11 @@ def select_provider_from_configs(
pod_config["provider"].get("type"), str
):
console.print(f"provider: {pod_config['provider']['type']}")
console.print(f"team: {team_id}")
console.print(f"team: {resolved_team_id}")
if disks:
console.print(f"disks: {', '.join(disks)}")
if sharing_display:
console.print(f"sharing: {sharing_display}")

if confirm_or_skip("\nDo you want to create this pod?", yes, default=True):
try:
Expand Down Expand Up @@ -943,7 +1043,7 @@ def connect(pod_id: str) -> None:
time.sleep(5) # Wait 5 seconds before retrying

# Get SSH key path from config
ssh_key_path = config.ssh_key_path
ssh_key_path = Config().ssh_key_path
if not os.path.exists(ssh_key_path):
console.print(f"[red]SSH key not found at {ssh_key_path}[/red]")
raise typer.Exit(1)
Expand Down
110 changes: 105 additions & 5 deletions packages/prime/src/prime_cli/commands/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from rich.console import Console
from rich.table import Table

from prime_cli.core import Config

from ..client import APIClient, APIError
from ..utils import output_data_as_json, validate_output_format

Expand All @@ -10,26 +12,62 @@


def fetch_teams(client: APIClient) -> list[dict]:
"""Fetch teams for the current user."""
response = client.get("/user/teams")
"""Fetch all teams for the current user across paginated responses."""
all_teams: list[dict] = []
offset = 0
limit = 100

while True:
response = client.get("/user/teams", params={"offset": offset, "limit": limit})
batch = response.get("data", []) if isinstance(response, dict) else []
all_teams.extend(batch)
total = response.get("total_count", len(all_teams)) if isinstance(response, dict) else 0

if not batch or len(all_teams) >= total:
break

offset += limit

return all_teams


def fetch_team_members(client: APIClient, team_id: str) -> list[dict]:
"""Fetch members of a team."""
response = client.get(f"/teams/{team_id}/members")
return response.get("data", []) if isinstance(response, dict) else []


@app.command(name="list")
def list_teams(
limit: int = typer.Option(100, help="Maximum number of teams to list"),
offset: int = typer.Option(0, help="Number of teams to skip"),
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
) -> None:
"""List teams for the current user."""
validate_output_format(output, console)

try:
client = APIClient()
teams = fetch_teams(client)
response = client.get("/user/teams", params={"offset": offset, "limit": limit})

teams = response.get("data", []) if isinstance(response, dict) else []
total_count = (
response.get("total_count", len(teams)) if isinstance(response, dict) else len(teams)
)

if output == "json":
output_data_as_json({"teams": teams, "total_count": len(teams)}, console)
output_data_as_json(
{
"teams": teams,
"total_count": total_count,
"offset": offset,
"limit": limit,
},
console,
)
return
table = Table(title=f"Teams (Total: {len(teams)})", show_lines=True)

table = Table(title=f"Teams (Total: {total_count})", show_lines=True)
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("Name", style="blue")
table.add_column("Slug", style="green")
Expand All @@ -47,6 +85,68 @@ def list_teams(

console.print(table)

if total_count > offset + limit:
remaining = total_count - (offset + limit)
console.print(
f"\n[yellow]Showing {limit} of {total_count} teams. "
f"Use --offset {offset + limit} to see the next "
f"{min(limit, remaining)} teams.[/yellow]"
)

except APIError as e:
console.print(f"[red]Error:[/red] {str(e)}")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Unexpected error:[/red] {str(e)}")
raise typer.Exit(1)


@app.command(name="members")
def list_members(
team_id: str = typer.Option(
None, "--team-id", help="Team ID (uses config team_id if not specified)"
),
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
) -> None:
"""List members of a team."""
validate_output_format(output, console)

config = Config()
resolved_team_id = team_id or config.team_id

if not resolved_team_id:
console.print(
"[red]Error: No team selected. "
"Use --team-id or set a team with 'prime config set-team-id'[/red]"
)
raise typer.Exit(1)

try:
client = APIClient()
members = fetch_team_members(client, resolved_team_id)

if output == "json":
output_data_as_json({"members": members, "total_count": len(members)}, console)
return

table = Table(title=f"Team Members (Total: {len(members)})", show_lines=True)
table.add_column("User ID", style="cyan", no_wrap=True)
table.add_column("Name", style="blue")
table.add_column("Email", style="green")
table.add_column("Role", style="yellow")
table.add_column("Joined", style="magenta")

for m in members:
table.add_row(
str(m.get("userId", "")),
m.get("userName") or "N/A",
m.get("userEmail") or "N/A",
m.get("role", ""),
str(m.get("joinedAt", "")),
)

console.print(table)

except APIError as e:
console.print(f"[red]Error:[/red] {str(e)}")
raise typer.Exit(1)
Expand Down
Loading
Loading