Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c73ab3c
feat: cleanup flash deploy/undeploy/build command output format
KAJdev Feb 9, 2026
7808376
fix: cleanup
KAJdev Feb 9, 2026
bb8478b
fix: nits
KAJdev Feb 10, 2026
36b0d2b
Merge branch 'main' into zeke/ae-2085-clean-up-flash-undeploy-flash-d…
deanq Feb 12, 2026
7dda9e9
Merge main into PR branch, resolve conflict in serverless.py
KAJdev Feb 14, 2026
895fab7
fix: make env endpoint table consistent
KAJdev Feb 19, 2026
3b7f6e3
refactor: unify CLI output style across all commands
KAJdev Feb 19, 2026
c851d39
fix: reduce dim overuse, keep output readable
KAJdev Feb 19, 2026
6eba39a
feat: format dates as human-readable local time
KAJdev Feb 19, 2026
bd9fb1f
fix: flash app delete takes positional name arg instead of --app flag
KAJdev Feb 19, 2026
3876e28
fix: improve flash env list and app list output
KAJdev Feb 19, 2026
52a8e87
fix: redesign list/get views with status dots, visual hierarchy, and …
KAJdev Feb 19, 2026
08df1fb
fix: cap builds shown in app get to 5 most recent
KAJdev Feb 19, 2026
d8276df
fix: resolve merge conflicts with main
KAJdev Feb 19, 2026
6c20d38
fix: deduplicate state_dot, remove generate_resource_table wrapper, f…
KAJdev Feb 19, 2026
861e131
format
KAJdev Feb 19, 2026
cfdbee1
fix: lint errors — remove empty f-strings and unused variable
KAJdev Feb 19, 2026
8fece22
Update src/runpod_flash/cli/utils/formatting.py
KAJdev Feb 20, 2026
2e552ba
Update src/runpod_flash/cli/utils/deployment.py
KAJdev Feb 20, 2026
d2d2e06
Update src/runpod_flash/cli/commands/apps.py
KAJdev Feb 20, 2026
c1a1a6e
fix: address PR review comments
KAJdev Feb 20, 2026
e52af3d
fix: merge remote copilot suggestion commits
KAJdev Feb 20, 2026
5a77431
format
KAJdev Feb 20, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,5 @@ cython_debug/
# dev only
.envrc
test_app/
pytest-results.xml
coverage.xml
129 changes: 67 additions & 62 deletions src/runpod_flash/cli/commands/apps.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"""Deployment environment management commands."""
"""CLI commands for managing Flash apps (create, get, list, delete)."""

import typer
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
import asyncio

from runpod_flash.cli.utils.app import discover_flash_project

import typer
from rich.console import Console

from runpod_flash.cli.utils.formatting import STATE_STYLE, format_datetime, state_dot
from runpod_flash.core.resources.app import FlashApp

console = Console()
Expand All @@ -35,101 +32,109 @@ def list_command():
"delete", short_help="Delete an existing flash app and all its associated resources"
)
def delete(
app_name: str = typer.Option(..., "--app", "-a", help="Flash app name to delete"),
app_name: str = typer.Argument(..., help="Name of the flash app to delete"),
):
if not app_name:
_, app_name = discover_flash_project()
return asyncio.run(delete_flash_app(app_name))


async def list_flash_apps():
apps = await FlashApp.list()
if not apps:
console.print("No Flash apps found.")
console.print("\nNo Flash apps found.")
console.print(" Run [bold]flash deploy[/bold] to create one.\n")
return

table = Table(show_header=True, header_style="bold")
table.add_column("Name", style="bold")
table.add_column("ID", overflow="fold")
table.add_column("Environments", overflow="fold")
table.add_column("Builds", overflow="fold")

for app in apps:
environments = app.get("flashEnvironments") or []
env_summary = ", ".join(env.get("name", "?") for env in environments) or "—"
builds = app.get("flashBuilds") or []
build_summary = ", ".join(build.get("id", "?") for build in builds) or "—"
table.add_row(
app.get("name", "(unnamed)"), app.get("id", "—"), env_summary, build_summary
console.print()
for app_data in apps:
name = app_data.get("name", "(unnamed)")
app_id = app_data.get("id", "")
environments = app_data.get("flashEnvironments") or []
builds = app_data.get("flashBuilds") or []

env_count = len(environments)
build_count = len(builds)
console.print(
f" [bold]{name}[/bold] "
f"{env_count} env{'s' if env_count != 1 else ''}, "
f"{build_count} build{'s' if build_count != 1 else ''} "
f"[dim]{app_id}[/dim]"
)

console.print(table)
for env in environments:
state = env.get("state", "UNKNOWN")
env_name = env.get("name", "?")
console.print(
f" {state_dot(state)} {env_name} [dim]{state.lower()}[/dim]"
)

console.print()


async def create_flash_app(app_name: str):
with console.status(f"Creating flash app: {app_name}"):
app = await FlashApp.create(app_name)

panel_content = (
f"Flash app '[bold]{app_name}[/bold]' created successfully\n\nApp ID: {app.id}"
console.print(
f"[green]✓[/green] Created app [bold]{app_name}[/bold] [dim]{app.id}[/dim]"
)
console.print(Panel(panel_content, title="✅ App Created", expand=False))


async def get_flash_app(app_name: str):
with console.status(f"Fetching flash app: {app_name}"):
app = await FlashApp.from_name(app_name)
# Fetch environments and builds in parallel for better performance
envs, builds = await asyncio.gather(app.list_environments(), app.list_builds())

main_info = f"Name: {app.name}\n"
main_info += f"ID: {app.id}\n"
main_info += f"Environments: {len(envs)}\n"
main_info += f"Builds: {len(builds)}"

console.print(Panel(main_info, title=f"📱 Flash App: {app_name}", expand=False))
console.print(f"\n [bold]{app.name}[/bold] [dim]{app.id}[/dim]")

# environments
console.print("\n [bold]Environments[/bold]")
if envs:
env_table = Table(title="Environments")
env_table.add_column("Name", style="cyan")
env_table.add_column("ID", overflow="fold")
env_table.add_column("State", style="yellow")
env_table.add_column("Active Build", overflow="fold")
env_table.add_column("Created", style="dim")

for env in envs:
env_table.add_row(
env.get("name"),
env.get("id", "-"),
env.get("state", "UNKNOWN"),
env.get("activeBuildId", "-"),
env.get("createdAt", "-"),
state = env.get("state", "UNKNOWN")
color = STATE_STYLE.get(state, "yellow")
name = env.get("name", "(unnamed)")
build_id = env.get("activeBuildId")
created = format_datetime(env.get("createdAt"))

console.print(
f" {state_dot(state)} [bold]{name}[/bold] "
f"[{color}]{state.lower()}[/{color}]"
)
console.print(env_table)
parts = []
if build_id:
parts.append(f"build {build_id}")
parts.append(f"created {created}")
console.print(f" [dim]{' · '.join(parts)}[/dim]")
else:
console.print(" [dim]None yet — run [/dim][bold]flash deploy[/bold]")

# builds — show most recent, summarize the rest
max_shown = 5
console.print(f"\n [bold]Builds ({len(builds)})[/bold]")
if builds:
build_table = Table(title="Builds")
build_table.add_column("ID", overflow="fold")
build_table.add_column("Object Key", overflow="fold")
build_table.add_column("Created", style="dim")

for build in builds:
build_table.add_row(
build.get("id"),
build.get("objectKey", "-"),
build.get("createdAt", "-"),
recent = builds[:max_shown]
for build in recent:
build_id = build.get("id", "")
created = format_datetime(build.get("createdAt"))
console.print(f" {build_id} [dim]{created}[/dim]")
if len(builds) > max_shown:
console.print(
f" [dim]… and {len(builds) - max_shown} older builds[/dim]"
)
console.print(build_table)
else:
console.print(" [dim]None yet — run [/dim][bold]flash build[/bold]")

console.print()


async def delete_flash_app(app_name: str):
with console.status(f"Deleting flash app: {app_name}"):
success = await FlashApp.delete(app_name=app_name)

if success:
console.print(f"✅ Flash app '{app_name}' deleted successfully")
console.print(f"[green]✓[/green] Deleted app [bold]{app_name}[/bold]")
else:
console.print(f" Failed to delete flash app '{app_name}'")
console.print(f"[red]✗[/red] Failed to delete app '{app_name}'")
raise typer.Exit(1)


Expand Down
1 change: 1 addition & 0 deletions src/runpod_flash/cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def run_build(
output_name: Custom archive name (default: artifact.tar.gz)
exclude: Comma-separated packages to exclude
use_local_flash: Bundle local runpod_flash source
verbose: Show archive and build directory paths in summary
Returns:
Path to the created artifact archive
Expand Down
133 changes: 60 additions & 73 deletions src/runpod_flash/cli/commands/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
import questionary
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from ..utils.app import discover_flash_project
from ..utils.formatting import STATE_STYLE, format_datetime, state_dot

from runpod_flash.core.resources.app import FlashApp

Expand Down Expand Up @@ -96,24 +95,31 @@ async def _list_environments(app_name: str):
envs = await app.list_environments()

if not envs:
console.print(f"No environments found for '{app_name}'.")
console.print(f"\nNo environments for [bold]{app_name}[/bold].")
console.print(" Run [bold]flash deploy[/bold] to create one.\n")
return

table = Table(show_header=True, header_style="bold")
table.add_column("Name", style="bold")
table.add_column("ID", overflow="fold")
table.add_column("Active Build", overflow="fold")
table.add_column("Created At", overflow="fold")

console.print(
f"\n [bold]{app_name}[/bold] {len(envs)} environment{'s' if len(envs) != 1 else ''}\n"
)
for env in envs:
table.add_row(
env.get("name"),
env.get("id"),
env.get("activeBuildId", "-"),
env.get("createdAt"),
name = env.get("name", "(unnamed)")
state = env.get("state", "UNKNOWN")
color = STATE_STYLE.get(state, "yellow")
build = env.get("activeBuildId")
created = format_datetime(env.get("createdAt"))

console.print(
f" {state_dot(state)} [bold]{name}[/bold] "
f"[{color}]{state.lower()}[/{color}]"
)
parts = []
if build:
parts.append(f"build {build}")
parts.append(f"created {created}")
console.print(f" [dim]{' · '.join(parts)}[/dim]")

console.print(table)
console.print()


def create_command(
Expand All @@ -134,27 +140,10 @@ def create_command(
async def _create_environment(app_name: str, env_name: str):
app, env = await FlashApp.create_environment_and_app(app_name, env_name)

panel_content = (
f"Environment '[bold]{env_name}[/bold]' created successfully\n\n"
f"App: {app_name}\n"
f"Environment ID: {env.get('id')}\n"
f"Status: {env.get('state', 'PENDING')}"
console.print(
f"[green]✓[/green] Created environment [bold]{env_name}[/bold] "
f"[dim]{env.get('id')}[/dim]"
)
console.print(Panel(panel_content, title="Environment Created", expand=False))

table = Table(show_header=True, header_style="bold")
table.add_column("Name", style="bold")
table.add_column("ID", overflow="fold")
table.add_column("Status", overflow="fold")
table.add_column("Created At", overflow="fold")

table.add_row(
env.get("name"),
env.get("id"),
env.get("state", "PENDING"),
env.get("createdAt", "Just now"),
)
console.print(table)


def get_command(
Expand All @@ -171,41 +160,45 @@ async def _get_environment(app_name: str, env_name: str):
app = await FlashApp.from_name(app_name)
env = await app.get_environment_by_name(env_name)

main_info = f"Environment: {env.get('name')}\n"
main_info += f"ID: {env.get('id')}\n"
main_info += f"State: {env.get('state', 'UNKNOWN')}\n"
main_info += f"Active Build: {env.get('activeBuildId', 'None')}\n"

if env.get("createdAt"):
main_info += f"Created: {env.get('createdAt')}\n"
state = env.get("state", "UNKNOWN")
color = STATE_STYLE.get(state, "yellow")

console.print(Panel(main_info, title=f"Environment: {env_name}", expand=False))
console.print(
f"\n {state_dot(state)} [bold]{env.get('name')}[/bold] "
f"[{color}]{state.lower()}[/{color}]"
)
console.print(f" [dim]id[/dim] {env.get('id')}")
console.print(f" [dim]app[/dim] {app_name}")
console.print(f" [dim]build[/dim] {env.get('activeBuildId') or 'none'}")

endpoints = env.get("endpoints") or []
network_volumes = env.get("networkVolumes") or []

if endpoints:
endpoint_table = Table(title="Associated Endpoints")
endpoint_table.add_column("Name", style="cyan")
endpoint_table.add_column("ID", overflow="fold")

for endpoint in endpoints:
endpoint_table.add_row(
endpoint.get("name", "-"),
endpoint.get("id", "-"),
console.print("\n [bold]Endpoints[/bold]")
for ep in endpoints:
console.print(
f" ▸ [bold]{ep.get('name', '-')}[/bold] [dim]{ep.get('id', '')}[/dim]"
)
console.print(endpoint_table)

network_volumes = env.get("networkVolumes") or []
if network_volumes:
nv_table = Table(title="Associated Network Volumes")
nv_table.add_column("Name", style="cyan")
nv_table.add_column("ID", overflow="fold")

console.print("\n [bold]Network Volumes[/bold]")
for nv in network_volumes:
nv_table.add_row(
nv.get("name", "-"),
nv.get("id", "-"),
console.print(
f" ▸ [bold]{nv.get('name', '-')}[/bold] [dim]{nv.get('id', '')}[/dim]"
)
console.print(nv_table)

if not endpoints and not network_volumes:
console.print("\n No resources deployed yet.")
console.print(f" Run [bold]flash deploy --env {env_name}[/bold] to deploy.")
else:
console.print("\n [bold]Commands[/bold]")
console.print(
f" [dim]flash deploy --env {env_name}[/dim] Update deployment"
)
console.print(f" [dim]flash env delete {env_name}[/dim] Tear down")

console.print()


def delete_command(
Expand All @@ -221,16 +214,10 @@ def delete_command(
try:
env = asyncio.run(_fetch_environment_info(app_name, env_name))
except Exception as e:
console.print(f"[red]Error:[/red] Failed to fetch environment info: {e}")
console.print(f"[red][/red] Failed to fetch environment info: {e}")
raise typer.Exit(1)

panel_content = (
f"Environment '[bold]{env_name}[/bold]' will be deleted\n\n"
f"Environment ID: {env.get('id')}\n"
f"App: {app_name}\n"
f"Active Build: {env.get('activeBuildId', 'None')}"
)
console.print(Panel(panel_content, title="Delete Confirmation", expand=False))
console.print(f"\nDeleting [bold]{env_name}[/bold] [dim]{env.get('id')}[/dim]")

try:
confirmed = questionary.confirm(
Expand All @@ -239,10 +226,10 @@ def delete_command(
).ask()

if not confirmed:
console.print("Deletion cancelled")
console.print("[yellow]Cancelled[/yellow]")
raise typer.Exit(0)
except KeyboardInterrupt:
console.print("\nDeletion cancelled")
console.print("\n[yellow]Cancelled[/yellow]")
raise typer.Exit(0)

asyncio.run(_delete_environment(app_name, env_name))
Expand All @@ -263,7 +250,7 @@ async def _delete_environment(app_name: str, env_name: str):
success = await app.delete_environment(env_name)

if success:
console.print(f"Environment '{env_name}' deleted successfully")
console.print(f"[green]✓[/green] Deleted environment [bold]{env_name}[/bold]")
else:
console.print(f"[red]Failed to delete environment '{env_name}'[/red]")
console.print(f"[red]✗[/red] Failed to delete environment '{env_name}'")
raise typer.Exit(1)
Loading
Loading