Files
dotfiles/.agents/skills/ios-simulator-skill/scripts/sim_list.py
2026-02-19 00:33:08 -08:00

300 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""
iOS Simulator Listing with Progressive Disclosure
Lists available simulators with token-efficient summaries.
Full details available on demand via cache IDs.
Achieves 96% token reduction (57k→2k tokens) for common queries.
Usage Examples:
# Concise summary (default)
python scripts/sim_list.py
# Get full details for cached list
python scripts/sim_list.py --get-details <cache-id>
# Get recommendations
python scripts/sim_list.py --suggest
# Filter by device type
python scripts/sim_list.py --device-type iPhone
Output (default):
Simulator Summary [cache-sim-20251028-143052]
├─ Total: 47 devices
├─ Available: 31
└─ Booted: 1
✓ iPhone 16 Pro (iOS 18.1) [ABC-123...]
Use --get-details cache-sim-20251028-143052 for full list
Technical Details:
- Uses xcrun simctl list devices
- Caches results with 1-hour TTL
- Reduces output by 96% by default
- Token efficiency: summary = ~30 tokens, full list = ~1500 tokens
"""
import argparse
import json
import subprocess
import sys
from typing import Any
from common import get_cache
class SimulatorLister:
"""Lists iOS simulators with progressive disclosure."""
def __init__(self):
"""Initialize lister with cache."""
self.cache = get_cache()
def list_simulators(self) -> dict:
"""
Get list of all simulators.
Returns:
Dict with structure:
{
"devices": [...],
"runtimes": [...],
"total_devices": int,
"available_devices": int,
"booted_devices": [...]
}
"""
try:
result = subprocess.run(
["xcrun", "simctl", "list", "devices", "--json"],
capture_output=True,
text=True,
check=True,
)
return json.loads(result.stdout)
except (subprocess.CalledProcessError, json.JSONDecodeError):
return {"devices": {}, "runtimes": []}
def parse_devices(self, sim_data: dict) -> list[dict]:
"""
Parse simulator data into flat list.
Returns:
List of device dicts with runtime info
"""
devices = []
devices_by_runtime = sim_data.get("devices", {})
for runtime_str, device_list in devices_by_runtime.items():
# Extract iOS version from runtime string
# Format: "iOS 18.1", "tvOS 18", etc.
runtime_name = runtime_str.replace(" Simulator", "").strip()
for device in device_list:
devices.append(
{
"name": device.get("name"),
"udid": device.get("udid"),
"state": device.get("state"),
"runtime": runtime_name,
"is_available": device.get("isAvailable", False),
}
)
return devices
def get_concise_summary(self, devices: list[dict]) -> dict:
"""
Generate concise summary with cache ID.
Returns 96% fewer tokens than full list.
"""
booted = [d for d in devices if d["state"] == "Booted"]
available = [d for d in devices if d["is_available"]]
iphone = [d for d in available if "iPhone" in d["name"]]
# Cache full list for later retrieval
cache_id = self.cache.save(
{
"devices": devices,
"timestamp": __import__("datetime").datetime.now().isoformat(),
},
"simulator-list",
)
return {
"cache_id": cache_id,
"summary": {
"total_devices": len(devices),
"available_devices": len(available),
"booted_devices": len(booted),
},
"quick_access": {
"booted": booted[:3] if booted else [],
"recommended_iphone": iphone[:3] if iphone else [],
},
}
def get_full_list(
self,
cache_id: str,
device_type: str | None = None,
runtime: str | None = None,
) -> list[dict] | None:
"""
Retrieve full simulator list from cache.
Args:
cache_id: Cache ID from concise summary
device_type: Filter by type (iPhone, iPad, etc.)
runtime: Filter by iOS version
Returns:
List of devices matching filters
"""
data = self.cache.get(cache_id)
if not data:
return None
devices = data.get("devices", [])
# Apply filters
if device_type:
devices = [d for d in devices if device_type in d["name"]]
if runtime:
devices = [d for d in devices if runtime.lower() in d["runtime"].lower()]
return devices
def suggest_simulators(self, limit: int = 4) -> list[dict]:
"""
Get simulator recommendations.
Returns:
List of recommended simulators (best candidates for building)
"""
all_sims = self.list_simulators()
devices = self.parse_devices(all_sims)
# Score devices for recommendations
scored = []
for device in devices:
score = 0
# Prefer booted
if device["state"] == "Booted":
score += 10
# Prefer available
if device["is_available"]:
score += 5
# Prefer recent iOS versions
ios_version = device["runtime"]
if "18" in ios_version:
score += 3
elif "17" in ios_version:
score += 2
# Prefer iPhones over other types
if "iPhone" in device["name"]:
score += 1
scored.append({"device": device, "score": score})
# Sort by score and return top N
scored.sort(key=lambda x: x["score"], reverse=True)
return [s["device"] for s in scored[:limit]]
def format_device(device: dict) -> str:
"""Format device for display."""
state_icon = "" if device["state"] == "Booted" else " "
avail_icon = "" if device["is_available"] else ""
name = device["name"]
runtime = device["runtime"]
udid_short = device["udid"][:8] + "..."
return f"{state_icon} {avail_icon} {name} ({runtime}) [{udid_short}]"
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="List iOS simulators with progressive disclosure")
parser.add_argument(
"--get-details",
metavar="CACHE_ID",
help="Get full details for cached simulator list",
)
parser.add_argument("--suggest", action="store_true", help="Get simulator recommendations")
parser.add_argument(
"--device-type",
help="Filter by device type (iPhone, iPad, Apple Watch, etc.)",
)
parser.add_argument("--runtime", help="Filter by iOS version (e.g., iOS-18, iOS-17)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
lister = SimulatorLister()
# Get full list with details
if args.get_details:
devices = lister.get_full_list(
args.get_details, device_type=args.device_type, runtime=args.runtime
)
if devices is None:
print(f"Error: Cache ID not found or expired: {args.get_details}")
sys.exit(1)
if args.json:
print(json.dumps(devices, indent=2))
else:
print(f"Simulators ({len(devices)}):\n")
for device in devices:
print(f" {format_device(device)}")
# Get recommendations
elif args.suggest:
suggestions = lister.suggest_simulators()
if args.json:
print(json.dumps(suggestions, indent=2))
else:
print("Recommended Simulators:\n")
for i, device in enumerate(suggestions, 1):
print(f"{i}. {format_device(device)}")
# Default: concise summary
else:
all_sims = lister.list_simulators()
devices = lister.parse_devices(all_sims)
summary = lister.get_concise_summary(devices)
if args.json:
print(json.dumps(summary, indent=2))
else:
# Human-readable concise output
cache_id = summary["cache_id"]
s = summary["summary"]
q = summary["quick_access"]
print(f"Simulator Summary [{cache_id}]")
print(f"├─ Total: {s['total_devices']} devices")
print(f"├─ Available: {s['available_devices']}")
print(f"└─ Booted: {s['booted_devices']}")
if q["booted"]:
print()
for device in q["booted"]:
print(f" {format_device(device)}")
print()
print(f"Use --get-details {cache_id} for full list")
if __name__ == "__main__":
main()