Skip to content
Open
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
113 changes: 91 additions & 22 deletions packages/prime/src/prime_cli/commands/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@

from ..utils import validate_output_format

app = typer.Typer(
help="Manage Docker images in Prime Intellect registry [closed beta]", no_args_is_help=True
)
app = typer.Typer(help="Manage Docker images in Prime Intellect registry", no_args_is_help=True)
console = Console()

config = Config()
Expand Down Expand Up @@ -56,6 +54,8 @@ def push_image(
console.print(
f"[bold blue]Building and pushing image:[/bold blue] {image_name}:{image_tag}"
)
if config.team_id:
console.print(f"[dim]Team: {config.team_id}[/dim]")
console.print()

# Initialize API client
Expand Down Expand Up @@ -83,15 +83,19 @@ def push_image(
# Initialize build
console.print("[cyan]Initiating build...[/cyan]")
try:
build_payload = {
"image_name": image_name,
"image_tag": image_tag,
"dockerfile_path": dockerfile,
"platform": platform,
}
if config.team_id:
build_payload["team_id"] = config.team_id

build_response = client.request(
"POST",
"/images/build",
json={
"image_name": image_name,
"image_tag": image_tag,
"dockerfile_path": dockerfile,
"platform": platform,
},
json=build_payload,
)
except UnauthorizedError:
console.print(
Expand Down Expand Up @@ -182,19 +186,31 @@ def push_image(
@app.command("list")
def list_images(
output: str = typer.Option("table", "--output", "-o", help="Output format (table or json)"),
all_images: bool = typer.Option(
False, "--all", "-a", help="Show all accessible images (personal + team)"
),
):
"""
List all images you've pushed to Prime Intellect registry.

By default, shows images in the current context (personal or team).
Use --all to show all accessible images including team images.

Examples:
prime images list
prime images list --all
prime images list --output json
"""
validate_output_format(output, console)
try:
client = APIClient()

response = client.request("GET", "/images")
# Build query params
params = {}
if config.team_id and not all_images:
params["teamId"] = config.team_id

response = client.request("GET", "/images", params=params if params else None)
images = response.get("data", [])

if not images:
Expand All @@ -207,8 +223,15 @@ def list_images(
return

# Table output
table = Table(title="Your Docker Images")
title = "Your Docker Images"
if config.team_id and not all_images:
title = f"Team Docker Images (team: {config.team_id})"
elif all_images:
title = "All Accessible Docker Images"

table = Table(title=title)
table.add_column("Image Reference", style="cyan")
table.add_column("Owner", justify="center")
table.add_column("Status", justify="center")
table.add_column("Size", justify="right")
table.add_column("Created", style="dim")
Expand All @@ -229,6 +252,13 @@ def list_images(
else:
status_display = f"[dim]{status}[/dim]"

# Owner type
owner_type = img.get("ownerType", "personal")
if owner_type == "team":
owner_display = "[blue]Team[/blue]"
else:
owner_display = "[dim]Personal[/dim]"

# Size
size_mb = ""
if img.get("sizeBytes"):
Expand All @@ -244,13 +274,14 @@ def list_images(
except Exception:
date_str = img.get("pushedAt") or img.get("createdAt", "")

# Image reference
# Image reference - prefer displayRef for user-friendly format
image_ref = (
img.get("fullImagePath")
img.get("displayRef")
or img.get("fullImagePath")
or f"{img.get('imageName', 'unknown')}:{img.get('imageTag', 'latest')}"
)

table.add_row(image_ref, status_display, size_mb, date_str)
table.add_row(image_ref, owner_display, status_display, size_mb, date_str)

console.print()
console.print(table)
Expand All @@ -269,22 +300,51 @@ def list_images(
@app.command("delete")
def delete_image(
image_reference: str = typer.Argument(
..., help="Image reference to delete (e.g., 'myapp:v1.0.0')"
...,
help="Image reference to delete (e.g., 'myapp:v1.0.0' or 'team-{teamId}/myapp:v1.0.0')",
),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
):
"""
Delete an image from your registry.

Note: This removes the database record but does not delete the actual
image from Google Artifact Registry.
For team images, you can use the team-prefixed format directly.
Only the image creator or team admins can delete team images.

Examples:
prime images delete myapp:v1.0.0
prime images delete myapp:latest --yes
prime images delete team-abc123/myapp:v1.0.0
"""
# Store original input for error messages
original_reference = image_reference

try:
# Parse image reference
# Check for team-prefixed format: team-{teamId}/imagename:tag
team_id = config.team_id
if "/" in image_reference:
namespace, rest = image_reference.split("/", 1)
if namespace.startswith("team-"):
# Extract team ID from the reference
extracted_team_id = namespace[5:] # Remove "team-" prefix
if not extracted_team_id:
console.print(
"[red]Error: Invalid team image reference. "
"Expected format: team-{teamId}/imagename:tag[/red]"
)
raise typer.Exit(1)
team_id = extracted_team_id
image_reference = rest
else:
# Unrecognized namespace (not team-prefixed)
console.print(
f"[red]Error: Unrecognized image namespace '{namespace}'. "
"Use 'imagename:tag' for personal images or "
"'team-{{teamId}}/imagename:tag' for team images.[/red]"
)
raise typer.Exit(1)

# Validate image reference has a tag (after team-prefix parsing)
if ":" not in image_reference:
console.print(
"[red]Error: Image reference must include a tag (e.g., myapp:latest)[/red]"
Expand All @@ -293,23 +353,32 @@ def delete_image(

image_name, image_tag = image_reference.rsplit(":", 1)

context = f" (team: {team_id})" if team_id else ""
if not yes:
confirm = typer.confirm(f"Are you sure you want to delete {image_name}:{image_tag}?")
msg = f"Are you sure you want to delete {image_name}:{image_tag}{context}?"
confirm = typer.confirm(msg)
if not confirm:
console.print("[yellow]Cancelled[/yellow]")
raise typer.Exit(0)

client = APIClient()

client.request("DELETE", f"/images/{image_name}/{image_tag}")
console.print(f"[green]✓[/green] Deleted {image_name}:{image_tag}")
params = {"teamId": team_id} if team_id else None

client.request("DELETE", f"/images/{image_name}/{image_tag}", params=params)
console.print(f"[green]✓[/green] Deleted {image_name}:{image_tag}{context}")

except UnauthorizedError:
console.print("[red]Error: Not authenticated. Please run 'prime login' first.[/red]")
raise typer.Exit(1)
except APIError as e:
if "404" in str(e):
console.print(f"[red]Error: Image {image_reference} not found[/red]")
console.print(f"[red]Error: Image {original_reference} not found[/red]")
elif "403" in str(e):
console.print(
"[red]Error: You don't have permission to delete this image. "
"Only the image creator or team admins can delete team images.[/red]"
)
else:
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
Loading