mycloudctl 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ """commands/folders.py -- mkdir, rm, rename, info commands."""
2
+ from __future__ import annotations
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+ from rich import box
10
+
11
+ from cli.api import CloudClient, APIError
12
+
13
+ app = typer.Typer(help="Folder management -- create and manage folders.")
14
+ console = Console()
15
+
16
+
17
+ def _client() -> CloudClient:
18
+ return CloudClient()
19
+
20
+
21
+ def _fmt(b: int | float | None) -> str:
22
+ import math
23
+ if b is None:
24
+ return "-"
25
+ b = float(b)
26
+ if b == 0:
27
+ return "0 B"
28
+ units = ["B", "KB", "MB", "GB", "TB"]
29
+ idx = min(int(math.log(b, 1024)), len(units) - 1)
30
+ return f"{b / (1024 ** idx):.1f} {units[idx]}"
31
+
32
+
33
+ @app.command("mkdir")
34
+ def mkdir(
35
+ name: str = typer.Argument(..., help="Folder name"),
36
+ parent_id: Optional[int] = typer.Option(None, "--parent", "-p", help="Parent folder ID"),
37
+ node: str = typer.Option("central", "--node", "-n", help="Storage node"),
38
+ ):
39
+ """Create a new folder."""
40
+ client = _client()
41
+ body: dict = {"folder_name": name}
42
+ if parent_id is not None:
43
+ body["parent_id"] = parent_id
44
+ if node != "central":
45
+ body["storage_mode"] = node
46
+
47
+ try:
48
+ result = client.post("/create_folder", json=body)
49
+ folder_id = result.get("folder_id") or result.get("id")
50
+ if result.get("success") or folder_id:
51
+ console.print(f"[green][OK][/] Folder '[bold]{name}[/]' created (id={folder_id}).")
52
+ else:
53
+ console.print(f"[red][FAIL][/] {result.get('message', 'Error')}")
54
+ except APIError as e:
55
+ console.print(f"[red][FAIL][/] {e}")
56
+
57
+
58
+ @app.command("rm")
59
+ def rmdir(
60
+ folder_id: int = typer.Argument(..., help="Folder ID to delete"),
61
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
62
+ ):
63
+ """Delete a folder and ALL its contents. Cannot be undone."""
64
+ if not yes:
65
+ typer.confirm(
66
+ f"Delete folder {folder_id} and all its contents? This cannot be undone.",
67
+ abort=True,
68
+ )
69
+ client = _client()
70
+ try:
71
+ result = client.post(f"/delete_folder/{folder_id}")
72
+ if result.get("success"):
73
+ console.print(f"[green][OK][/] Folder {folder_id} deleted.")
74
+ else:
75
+ console.print(f"[red][FAIL][/] {result.get('message', 'Error')}")
76
+ except APIError as e:
77
+ console.print(f"[red][FAIL][/] {e}")
78
+
79
+
80
+ @app.command("rename")
81
+ def rename_folder(
82
+ folder_id: int = typer.Argument(..., help="Folder ID to rename"),
83
+ new_name: str = typer.Argument(..., help="New folder name"),
84
+ ):
85
+ """Rename a folder."""
86
+ client = _client()
87
+ try:
88
+ result = client.post(f"/rename_folder/{folder_id}", json={"name": new_name})
89
+ if result.get("success"):
90
+ actual = result.get("new_name", new_name)
91
+ console.print(f"[green][OK][/] Folder renamed to '[bold]{actual}[/]'.")
92
+ else:
93
+ console.print(f"[red][FAIL][/] {result.get('message', 'Error')}")
94
+ except APIError as e:
95
+ console.print(f"[red][FAIL][/] {e}")
96
+
97
+
98
+ @app.command("info")
99
+ def folder_info(
100
+ folder_id: int = typer.Argument(..., help="Folder ID"),
101
+ ):
102
+ """Show detailed info about a folder."""
103
+ client = _client()
104
+ try:
105
+ data = client.get(f"/api/folder_details/{folder_id}")
106
+ except APIError as e:
107
+ console.print(f"[red][FAIL][/] {e}")
108
+ raise typer.Exit(1)
109
+
110
+ folder = data.get("folder") or data
111
+ files = data.get("files", [])
112
+ subfolders = data.get("subfolders") or data.get("folders", [])
113
+
114
+ console.print(Panel(
115
+ f"[bold]{folder.get('name', '?')}[/bold]\n"
116
+ f"[dim]ID:[/dim] {folder.get('id', '?')}\n"
117
+ f"[dim]Parent ID:[/dim] {folder.get('parent_id') or 'root'}\n"
118
+ f"[dim]Created:[/dim] {str(folder.get('created_at', '?'))[:16]}\n"
119
+ f"[dim]Storage:[/dim] {folder.get('storage_mode') or 'central'}\n"
120
+ f"[dim]Files:[/dim] {len(files)}\n"
121
+ f"[dim]Subfolders:[/dim] {len(subfolders)}",
122
+ title=f"[bold]Folder Details[/bold]",
123
+ border_style="magenta",
124
+ ))
125
+
126
+ if subfolders:
127
+ t = Table(title="Subfolders", box=box.SIMPLE, header_style="bold magenta")
128
+ t.add_column("ID", style="dim", justify="right")
129
+ t.add_column("Name")
130
+ for sf in subfolders:
131
+ t.add_row(str(sf.get("id", "?")), sf.get("name", "-"))
132
+ console.print(t)
133
+
134
+ if files:
135
+ t = Table(title="Files", box=box.SIMPLE, header_style="bold cyan")
136
+ t.add_column("ID", style="dim", justify="right")
137
+ t.add_column("Name")
138
+ t.add_column("Size", justify="right", style="green")
139
+ for f in files:
140
+ t.add_row(
141
+ str(f.get("id", "?")),
142
+ f.get("original_filename") or f.get("name") or "-",
143
+ _fmt(f.get("file_size") or f.get("size")),
144
+ )
145
+ console.print(t)
146
+
147
+
148
+ @app.command("mv")
149
+ def mv(
150
+ folder_id: int = typer.Argument(..., help="Folder ID to move"),
151
+ dest_id: int = typer.Argument(..., help="Destination parent folder ID (0 for root)"),
152
+ ):
153
+ """Move a folder to another parent folder."""
154
+ client = _client()
155
+ try:
156
+ dest_val = None if dest_id == 0 else dest_id
157
+ result = client.post("/api/move_folder", json={
158
+ "folder_id": folder_id,
159
+ "destination_folder_id": dest_val
160
+ })
161
+ if result.get("success"):
162
+ console.print(f"[green][OK][/] Folder {folder_id} moved to folder {dest_id}.")
163
+ else:
164
+ console.print(f"[red][FAIL][/] {result.get('error') or result.get('message', 'Move failed')}")
165
+ except APIError as e:
166
+ console.print(f"[red][FAIL][/] {e}")
167
+
168
+
169
+ @app.command("download")
170
+ def download(
171
+ folder_id: int = typer.Argument(..., help="Folder ID to download"),
172
+ output: str = typer.Option(".", "--output", "-o", help="Output directory"),
173
+ ):
174
+ """Download a folder and its contents as a zip archive."""
175
+ from pathlib import Path
176
+ client = _client()
177
+ out_dir = Path(output)
178
+
179
+ console.print(f"[cyan]Preparing download for folder {folder_id}[/]")
180
+ try:
181
+ path = client.download_folder(folder_id, out_dir)
182
+ console.print(f"[green][OK][/] Saved to [bold]{path}[/bold]")
183
+ except APIError as e:
184
+ console.print(f"[red][FAIL][/] {e}")
185
+
186
+
187
+ @app.command("favorite")
188
+ def favorite(
189
+ folder_id: int = typer.Argument(..., help="Folder ID"),
190
+ ):
191
+ """Toggle favorite status for a folder."""
192
+ client = _client()
193
+ try:
194
+ result = client.post(f"/api/folders/{folder_id}/favorite")
195
+ state = result.get("is_favorite")
196
+ icon = "[red][/]" if state else "[dim][/]"
197
+ label = "Favorited" if state else "Un-favorited"
198
+ console.print(f"{icon} {label} folder {folder_id}.")
199
+ except APIError as e:
200
+ console.print(f"[red][FAIL][/] {e}")
@@ -0,0 +1,79 @@
1
+ """commands/notifications.py list and mark-read notifications."""
2
+ from __future__ import annotations
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cli.api import CloudClient, APIError
9
+ from cli.ui.table import notifications_table
10
+
11
+ app = typer.Typer(help="Notifications view and manage your notifications.")
12
+ console = Console()
13
+
14
+
15
+ def _client() -> CloudClient:
16
+ return CloudClient()
17
+
18
+
19
+ @app.command("ls")
20
+ def notif_ls(
21
+ limit: int = typer.Option(20, "--limit", "-l", help="Max notifications to show"),
22
+ ):
23
+ """List your notifications."""
24
+ client = _client()
25
+ try:
26
+ data = client.get("/api/notifications", limit=limit)
27
+ except APIError as e:
28
+ console.print(f"[red]-[/] {e}")
29
+ raise typer.Exit(1)
30
+
31
+ notifs = data.get("notifications", []) or data.get("data", [])
32
+ if not notifs:
33
+ console.print("[dim]No notifications.[/dim]")
34
+ return
35
+ console.print(notifications_table(notifs))
36
+
37
+
38
+ @app.command("read")
39
+ def notif_read(
40
+ ids: Optional[str] = typer.Argument(None,
41
+ help="Comma-separated notification IDs to mark read, or omit to mark all."),
42
+ ):
43
+ """Mark notifications as read."""
44
+ client = _client()
45
+ body: dict = {}
46
+ if ids:
47
+ id_list = [int(x.strip()) for x in ids.split(",") if x.strip().isdigit()]
48
+ body["notification_ids"] = id_list
49
+
50
+ try:
51
+ result = client.post("/api/notifications/mark_read", json=body)
52
+ if result.get("success"):
53
+ console.print("[green][OK][/] Notifications marked as read.")
54
+ else:
55
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
56
+ except APIError as e:
57
+ console.print(f"[red]-[/] {e}")
58
+
59
+
60
+ @app.command("hide")
61
+ def notif_hide(
62
+ ids: str = typer.Argument(..., help="Comma-separated notification IDs to hide"),
63
+ ):
64
+ """Permanently hide notifications by ID."""
65
+ client = _client()
66
+ id_list = [int(x.strip()) for x in ids.split(",") if x.strip().isdigit()]
67
+ if not id_list:
68
+ console.print("[yellow]No valid IDs provided.[/yellow]")
69
+ raise typer.Exit(1)
70
+
71
+ try:
72
+ result = client.post("/api/notifications/hide", json={"notification_ids": id_list})
73
+ if result.get("success"):
74
+ console.print(f"[green][OK][/] {len(id_list)} notification(s) hidden.")
75
+ else:
76
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
77
+ except APIError as e:
78
+ console.print(f"[red]-[/] {e}")
79
+
cli/commands/prefs.py ADDED
@@ -0,0 +1,72 @@
1
+ """commands/prefs.py -- user preferences management."""
2
+ from __future__ import annotations
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+ from rich import box
9
+
10
+ from cli.api import CloudClient, APIError
11
+
12
+ app = typer.Typer(help="Preferences -- view and update your account preferences.")
13
+ console = Console()
14
+
15
+
16
+ def _client() -> CloudClient:
17
+ return CloudClient()
18
+
19
+
20
+ @app.command("show")
21
+ def prefs_show():
22
+ """Display all your current preferences."""
23
+ client = _client()
24
+ try:
25
+ data = client.get("/api/user_preferences")
26
+ except APIError as e:
27
+ console.print(f"[red][FAIL][/] {e}")
28
+ raise typer.Exit(1)
29
+
30
+ prefs = data.get("preferences") or data.get("data") or data
31
+ if not prefs or not isinstance(prefs, dict):
32
+ console.print("[dim]No preferences found.[/dim]")
33
+ return
34
+
35
+ t = Table(title="Your Preferences", box=box.ROUNDED, header_style="bold cyan")
36
+ t.add_column("Key", style="bold")
37
+ t.add_column("Value")
38
+
39
+ for k, v in prefs.items():
40
+ t.add_row(str(k), str(v))
41
+ console.print(t)
42
+
43
+
44
+ @app.command("set")
45
+ def prefs_set(
46
+ key: str = typer.Argument(..., help="Preference key"),
47
+ value: str = typer.Argument(..., help="New value"),
48
+ ):
49
+ """Set a user preference key to a value."""
50
+ client = _client()
51
+ try:
52
+ result = client.post("/api/user_preferences", json={key: value})
53
+ if result.get("success") is not False:
54
+ console.print(f"[green][OK][/] Preference '[bold]{key}[/]' set to '[bold]{value}[/]'.")
55
+ else:
56
+ console.print(f"[red][FAIL][/] {result.get('message', 'Error')}")
57
+ except APIError as e:
58
+ console.print(f"[red][FAIL][/] {e}")
59
+
60
+
61
+ @app.command("sync")
62
+ def prefs_sync():
63
+ """Sync preferences across devices."""
64
+ client = _client()
65
+ try:
66
+ result = client.post("/api/user_preferences/sync")
67
+ if result.get("success") is not False:
68
+ console.print("[green][OK][/] Preferences synced.")
69
+ else:
70
+ console.print(f"[red][FAIL][/] {result.get('message', 'Error')}")
71
+ except APIError as e:
72
+ console.print(f"[red][FAIL][/] {e}")
@@ -0,0 +1,157 @@
1
+ """commands/profile.py view/update profile, 2FA toggle, change password."""
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.prompt import Prompt
10
+
11
+ from cli.api import CloudClient, APIError
12
+ from cli.config import get_stored_user
13
+
14
+ app = typer.Typer(help="Profile view and update your account settings.")
15
+ console = Console()
16
+
17
+
18
+ def _client() -> CloudClient:
19
+ return CloudClient()
20
+
21
+
22
+ @app.command("show")
23
+ def profile_show():
24
+ """Display your profile information."""
25
+ user = get_stored_user()
26
+ if not user:
27
+ console.print("[yellow]Not logged in.[/yellow]")
28
+ raise typer.Exit(1)
29
+
30
+ client = _client()
31
+ try:
32
+ data = client.get("/settings")
33
+ except APIError as e:
34
+ # Fall back to cached data
35
+ data = {}
36
+
37
+ u = data.get("user") or user
38
+
39
+ console.print(Panel(
40
+ f"[bold]{u.get('name', '?')}[/bold]\n"
41
+ f"[dim]Username:[/dim] {u.get('username', '?')}\n"
42
+ f"[dim]Email:[/dim] {u.get('email', '?')}\n"
43
+ f"[dim]2FA:[/dim] {'[green]enabled[/]' if u.get('two_factor_enabled') else '[dim]disabled[/]'}\n"
44
+ f"[dim]Member since:[/dim] {str(u.get('created_at', '?'))[:10]}",
45
+ title="[bold]Your Profile[/bold]",
46
+ border_style="cyan",
47
+ ))
48
+
49
+
50
+ @app.command("update")
51
+ def profile_update(
52
+ name: Optional[str] = typer.Option(None, "--name", help="Display name"),
53
+ username: Optional[str] = typer.Option(None, "--username", help="New username"),
54
+ ):
55
+ """Update your display name or username."""
56
+ if not name and not username:
57
+ console.print("[yellow]Provide --name or --username to update.[/yellow]")
58
+ raise typer.Exit(1)
59
+
60
+ client = _client()
61
+ body: dict = {}
62
+ if name:
63
+ body["name"] = name
64
+ if username:
65
+ body["username"] = username
66
+
67
+ try:
68
+ result = client.post("/update_profile", json=body)
69
+ if result.get("success"):
70
+ console.print("[green][/] Profile updated.")
71
+ else:
72
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
73
+ except APIError as e:
74
+ console.print(f"[red]-[/] {e}")
75
+
76
+
77
+ @app.command("2fa")
78
+ def profile_2fa(
79
+ enable: Optional[bool] = typer.Option(None, "--enable/--disable",
80
+ help="Enable or disable 2FA"),
81
+ ):
82
+ """Toggle two-factor authentication."""
83
+ if enable is None:
84
+ user = get_stored_user()
85
+ current = bool(user.get("two_factor_enabled"))
86
+ enable = not current
87
+ console.print(f"[dim]Currently: {'enabled' if current else 'disabled'}. Toggling[/dim]")
88
+
89
+ client = _client()
90
+ try:
91
+ result = client.post("/settings/two-factor", json={"enabled": enable})
92
+ if result.get("success"):
93
+ state = "enabled" if enable else "disabled"
94
+ console.print(f"[green][/] 2FA {state}.")
95
+ else:
96
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
97
+ except APIError as e:
98
+ console.print(f"[red]-[/] {e}")
99
+
100
+
101
+ @app.command("avatar-upload")
102
+ def avatar_upload(
103
+ image: Path = typer.Argument(..., help="Path to the image file"),
104
+ ):
105
+ """Upload a new profile picture."""
106
+ if not image.exists() or not image.is_file():
107
+ console.print(f"[red]File not found:[/] {image}")
108
+ raise typer.Exit(1)
109
+
110
+ client = _client()
111
+ try:
112
+ result = client.upload_avatar(image)
113
+ if result.get("success"):
114
+ console.print("[green][OK][/] Profile picture uploaded successfully.")
115
+ else:
116
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
117
+ except APIError as e:
118
+ console.print(f"[red]-[/] {e}")
119
+
120
+
121
+ @app.command("avatar-rm")
122
+ def avatar_remove():
123
+ """Remove your current profile picture."""
124
+ client = _client()
125
+ try:
126
+ result = client.post("/remove_pfp")
127
+ if result.get("success"):
128
+ console.print("[green][OK][/] Profile picture removed.")
129
+ else:
130
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
131
+ except APIError as e:
132
+ console.print(f"[red]-[/] {e}")
133
+
134
+
135
+ @app.command("delete-account")
136
+ def profile_delete_account(
137
+ confirm: bool = typer.Option(False, "--yes", "-y", help="Confirm deletion without prompting"),
138
+ ):
139
+ """Permanently delete your account and all associated data."""
140
+ if not confirm:
141
+ sure = Prompt.ask("[bold red]Are you absolutely sure you want to delete your account? This cannot be undone[/bold red] (y/N)")
142
+ if sure.lower() not in ('y', 'yes'):
143
+ console.print("Account deletion aborted.")
144
+ raise typer.Exit()
145
+
146
+ client = _client()
147
+ try:
148
+ result = client.post("/delete_account")
149
+ if result.get("success"):
150
+ console.print(f"[green][OK][/] {result.get('message', 'Account deleted.')}")
151
+ # Clear local config since we are logged out
152
+ from cli.config import clear_credentials
153
+ clear_credentials()
154
+ else:
155
+ console.print(f"[red]-[/] {result.get('error', 'Error deleting account.')}")
156
+ except APIError as e:
157
+ console.print(f"[red]-[/] {e}")
cli/commands/search.py ADDED
@@ -0,0 +1,78 @@
1
+ """commands/search.py live search (find) command."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich import box
8
+
9
+ from cli.api import CloudClient, APIError
10
+
11
+ app = typer.Typer(help="Search files and folders.")
12
+ console = Console()
13
+
14
+
15
+ def _client() -> CloudClient:
16
+ return CloudClient()
17
+
18
+
19
+ @app.command("find")
20
+ def find(
21
+ query: str = typer.Argument(..., help="Search query"),
22
+ node: str = typer.Option("central", "--node", "-n", help="Node to search in"),
23
+ ):
24
+ """Search for files and folders by name."""
25
+ client = _client()
26
+ try:
27
+ data = client.get("/live_search", q=query, node=node)
28
+ except APIError as e:
29
+ console.print(f"[red]-[/] {e}")
30
+ raise typer.Exit(1)
31
+
32
+ if isinstance(data, list):
33
+ files = [item for item in data if item.get("item_type") == "file"]
34
+ folders = [item for item in data if item.get("item_type") == "folder"]
35
+ else:
36
+ files = data.get("files", [])
37
+ folders = data.get("folders", [])
38
+
39
+ if not files and not folders:
40
+ console.print(f"[dim]No results for '[bold]{query}[/]'.[/dim]")
41
+ return
42
+
43
+ t = Table(
44
+ title=f"Results for '{query}'",
45
+ box=box.ROUNDED,
46
+ header_style="bold cyan",
47
+ )
48
+ t.add_column("ID", justify="right", style="dim")
49
+ t.add_column("Type")
50
+ t.add_column("Name")
51
+ t.add_column("Size / Created", style="dim")
52
+
53
+ for fo in folders:
54
+ t.add_row(
55
+ str(fo.get("id", "?")),
56
+ " folder",
57
+ fo.get("name") or "",
58
+ str(fo.get("created_at", ""))[:16],
59
+ )
60
+ for f in files:
61
+ size = f.get("size") or f.get("file_size") or 0
62
+ t.add_row(
63
+ str(f.get("id", "?")),
64
+ " file",
65
+ f.get("filename") or f.get("name") or "",
66
+ _fmt(size),
67
+ )
68
+ console.print(t)
69
+ console.print(f"[dim]{len(folders)} folder(s) {len(files)} file(s)[/dim]")
70
+
71
+
72
+ def _fmt(b: int) -> str:
73
+ import math
74
+ units = ["B", "KB", "MB", "GB"]
75
+ if b == 0:
76
+ return "0 B"
77
+ idx = min(int(math.log(max(b, 1), 1024)), len(units) - 1)
78
+ return f"{b / (1024**idx):.1f} {units[idx]}"
@@ -0,0 +1,95 @@
1
+ """commands/servers.py private node (server) management."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+ from rich import box
9
+
10
+ from cli.api import CloudClient, APIError
11
+
12
+ app = typer.Typer(help="Private servers (nodes) list, add, and remove.")
13
+ console = Console()
14
+
15
+
16
+ def _client() -> CloudClient:
17
+ return CloudClient()
18
+
19
+
20
+ @app.command("ls")
21
+ def servers_ls():
22
+ """List your registered private servers (nodes)."""
23
+ client = _client()
24
+ try:
25
+ data = client.get("/settings/servers")
26
+ except APIError as e:
27
+ console.print(f"[red]-[/] {e}")
28
+ raise typer.Exit(1)
29
+
30
+ nodes = data.get("nodes", []) or data.get("servers", [])
31
+ if not nodes:
32
+ console.print("[dim]No private servers registered.[/dim]")
33
+ return
34
+
35
+ t = Table(title="Private Servers", box=box.ROUNDED, header_style="bold magenta")
36
+ t.add_column("ID", justify="right", style="dim")
37
+ t.add_column("Name")
38
+ t.add_column("Status")
39
+ t.add_column("Last Seen", style="dim")
40
+
41
+ for n in nodes:
42
+ status = n.get("status") or n.get("is_online")
43
+ if status in (True, 1, "online"):
44
+ status_str = "[green]- online[/]"
45
+ else:
46
+ status_str = "[red]- offline[/]"
47
+ t.add_row(
48
+ str(n.get("id", "?")),
49
+ n.get("name") or n.get("label") or "?",
50
+ status_str,
51
+ str(n.get("last_seen") or "")[:16],
52
+ )
53
+ console.print(t)
54
+
55
+
56
+ @app.command("add")
57
+ def servers_add():
58
+ """Generate a new API key for a private server node."""
59
+ client = _client()
60
+ try:
61
+ result = client.post("/generate_node_key")
62
+ key = result.get("key") or result.get("node_key") or result.get("api_key")
63
+ node_id = result.get("node_id") or result.get("id")
64
+ if key:
65
+ console.print(Panel(
66
+ f"[bold]Node ID:[/bold] {node_id}\n"
67
+ f"[bold]API Key:[/bold] [cyan]{key}[/cyan]\n\n"
68
+ "[dim]Copy this key into your node's config. It will not be shown again.[/dim]",
69
+ title="[bold green]Node Created[/bold green]",
70
+ border_style="green",
71
+ ))
72
+ else:
73
+ console.print(f"[red]-[/] {result.get('message', 'Error generating key')}")
74
+ except APIError as e:
75
+ console.print(f"[red]-[/] {e}")
76
+
77
+
78
+
79
+ @app.command("rm")
80
+ def servers_rm(
81
+ node_id: int = typer.Argument(..., help="Node ID to remove"),
82
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
83
+ ):
84
+ """Remove a private server node."""
85
+ if not yes:
86
+ typer.confirm(f"Remove node {node_id}?", abort=True)
87
+ client = _client()
88
+ try:
89
+ result = client.post(f"/delete_node/{node_id}")
90
+ if result.get("success"):
91
+ console.print(f"[green][/] Node {node_id} removed.")
92
+ else:
93
+ console.print(f"[red]-[/] {result.get('message', 'Error')}")
94
+ except APIError as e:
95
+ console.print(f"[red]-[/] {e}")