diff --git a/truffile/cli.py b/truffile/cli.py index 757243d..fa61753 100644 --- a/truffile/cli.py +++ b/truffile/cli.py @@ -601,6 +601,101 @@ async def cmd_list_apps(storage: StorageService) -> int: finally: await client.close() +async def cmd_delete(args, storage: StorageService) -> int: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError as e: + spinner.fail(str(e)) + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + try: + await client.connect() + foreground, background = await client.get_all_apps() + spinner.stop(success=True) + + all_apps = [] + for app in foreground: + all_apps.append(("focus", app.uuid, app.metadata.name, app.metadata.description.strip().split('\n')[0][:55] if app.metadata.description else "")) + for app in background: + all_apps.append(("ambient", app.uuid, app.metadata.name, app.metadata.description.strip().split('\n')[0][:55] if app.metadata.description else "")) + + if not all_apps: + print(f" {C.DIM}No apps installed{C.RESET}") + return 0 + + print() + print(f"{C.BOLD}Installed Apps:{C.RESET}") + print() + for i, (kind, uuid, name, desc) in enumerate(all_apps, 1): + print(f" {C.CYAN}{i}.{C.RESET} {name} {C.DIM}({kind}){C.RESET}") + if desc: + print(f" {C.DIM}{desc}{C.RESET}") + print() + + try: + choice = input(f"Select apps to delete (e.g. 1,3,5 or 'all'): ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + + if not choice: + return 0 + + if choice.lower() == "all": + to_delete = list(range(len(all_apps))) + else: + try: + to_delete = [int(x.strip()) - 1 for x in choice.split(",")] + for idx in to_delete: + if idx < 0 or idx >= len(all_apps): + error(f"Invalid selection: {idx + 1}") + return 1 + except ValueError: + error("Invalid input") + return 1 + + print() + deleted = 0 + for idx in to_delete: + kind, uuid, name, _ = all_apps[idx] + spinner = Spinner(f"Deleting {name}") + spinner.start() + try: + await client.delete_app(uuid) + spinner.stop(success=True) + deleted += 1 + except Exception as e: + spinner.fail(f"Failed to delete {name}: {e}") + + print() + success(f"Deleted {deleted} app(s)") + return 0 + + except Exception as e: + spinner.fail(str(e)) + return 1 + finally: + await client.close() + + async def _interactive_shell(ws_url: str) -> int: print(f"{C.DIM}Opening shell... (exit with Ctrl+D or 'exit'){C.RESET}") import os, termios, fcntl, struct, tty, contextlib, json @@ -976,6 +1071,7 @@ def print_help(): print(f" {C.BLUE}connect{C.RESET} Connect to a Truffle device") print(f" {C.BLUE}disconnect{C.RESET} Disconnect and clear credentials") print(f" {C.BLUE}deploy{C.RESET} [path] Deploy an app (reads type from truffile.yaml)") + print(f" {C.BLUE}delete{C.RESET} Delete installed apps from device") print(f" {C.BLUE}list{C.RESET} List installed apps or devices") print(f" {C.BLUE}models{C.RESET} List AI models on connected device") print(f" {C.BLUE}proxy{C.RESET} Start OpenAI-compatible inference proxy") @@ -1017,6 +1113,8 @@ def main() -> int: p_deploy.add_argument("path", nargs="?", default=".") p_deploy.add_argument("-i", "--interactive", action="store_true", help="Interactive terminal mode") + p_delete = subparsers.add_parser("delete", add_help=False) + p_list = subparsers.add_parser("list", add_help=False) p_list.add_argument("what", choices=["apps", "devices"], nargs="?") @@ -1058,6 +1156,8 @@ def main() -> int: return run_async(cmd_connect(args, storage)) elif args.command == "disconnect": return cmd_disconnect(args, storage) + elif args.command == "delete": + return run_async(cmd_delete(args, storage)) elif args.command == "deploy": return run_async(cmd_deploy(args, storage)) elif args.command == "list": diff --git a/truffile/client.py b/truffile/client.py index faefdce..1608159 100644 --- a/truffile/client.py +++ b/truffile/client.py @@ -22,7 +22,7 @@ NewSessionStatus, ) from truffle.os.client_metadata_pb2 import ClientMetadata -from truffle.os.app_queries_pb2 import GetAllAppsRequest, GetAllAppsResponse +from truffle.os.app_queries_pb2 import GetAllAppsRequest, GetAllAppsResponse, DeleteAppRequest, DeleteAppResponse from truffle.app.app_type_pb2 import AppType from truffle.app.foreground_pb2 import ForegroundApp from truffle.app.background_pb2 import BackgroundApp, BackgroundAppRuntimePolicy @@ -120,6 +120,14 @@ async def get_all_apps(self) -> tuple[list[ForegroundApp], list[BackgroundApp]]: resp: GetAllAppsResponse = await self.stub.Apps_GetAll(req, metadata=self._metadata) return list(resp.foreground_apps), list(resp.background_apps) + async def delete_app(self, app_uuid: str) -> DeleteAppResponse: + if not self.stub: + raise RuntimeError("not connected") + req = DeleteAppRequest() + req.app_uuid = app_uuid + resp: DeleteAppResponse = await self.stub.Apps_DeleteApp(req, metadata=self._metadata) + return resp + async def start_build(self, app_type: AppType = AppType.APP_TYPE_BACKGROUND) -> StartBuildSessionResponse: if not self.stub: raise RuntimeError("not connected")