bsm-cli 1.6.0b3__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.
bsm_cli/__init__.py ADDED
File without changes
bsm_cli/__main__.py ADDED
@@ -0,0 +1,88 @@
1
+ try:
2
+ import click
3
+
4
+ except ImportError:
5
+ print(
6
+ "Please install the required dependencies with `pip install bsm-api-client[cli]`"
7
+ )
8
+ exit(1)
9
+
10
+
11
+ from contextlib import asynccontextmanager
12
+
13
+ from bsm_cli.account import account
14
+ from bsm_cli.addon import addon
15
+ from bsm_cli.allowlist import allowlist
16
+ from bsm_cli.auth import auth
17
+ from bsm_cli.backup import backup
18
+ from bsm_cli.bans import bans
19
+ from bsm_cli.config import Config
20
+ from bsm_cli.content import content
21
+ from bsm_cli.decorators import AsyncGroup
22
+ from bsm_cli.main_menus import main_menu
23
+ from bsm_cli.permissions import permissions
24
+ from bsm_cli.player import player
25
+ from bsm_cli.plugins import plugin
26
+ from bsm_cli.properties import properties
27
+ from bsm_cli.server import server
28
+ from bsm_cli.system import system
29
+ from bsm_cli.users import users
30
+ from bsm_cli.world import world
31
+
32
+ from bsm_api_client import BedrockServerManagerApi
33
+
34
+
35
+ @click.group(cls=AsyncGroup, invoke_without_command=True)
36
+ @click.pass_context
37
+ def cli(ctx):
38
+ """A CLI for managing Bedrock servers."""
39
+ ctx.obj["cli"] = cli
40
+ if ctx.invoked_subcommand is None:
41
+ return main_menu(ctx)
42
+
43
+
44
+ @cli.context
45
+ @asynccontextmanager
46
+ async def cli_context(ctx):
47
+ config = Config()
48
+ ctx.obj["config"] = config
49
+
50
+ try:
51
+ client = BedrockServerManagerApi(
52
+ base_url=config.base_url,
53
+ jwt_token=config.jwt_token,
54
+ verify_ssl=config.verify_ssl,
55
+ )
56
+ except ValueError as e:
57
+ # Ignore AuthError when logging out or auth group is called
58
+ if ctx.invoked_subcommand == auth or ctx.invoked_subcommand is None:
59
+ client = None
60
+ else:
61
+ raise e
62
+ ctx.obj["client"] = client
63
+
64
+ try:
65
+ yield
66
+ finally:
67
+ if ctx.obj.get("client"):
68
+ await ctx.obj["client"].close()
69
+
70
+
71
+ cli.add_command(auth)
72
+ cli.add_command(server)
73
+ cli.add_command(addon)
74
+ cli.add_command(backup)
75
+ cli.add_command(player)
76
+ cli.add_command(plugin)
77
+ cli.add_command(allowlist)
78
+ cli.add_command(bans)
79
+ cli.add_command(permissions)
80
+ cli.add_command(properties)
81
+ cli.add_command(system)
82
+ cli.add_command(world)
83
+ cli.add_command(account)
84
+ cli.add_command(content)
85
+ cli.add_command(users)
86
+
87
+ if __name__ == "__main__":
88
+ cli()
bsm_cli/account.py ADDED
@@ -0,0 +1,75 @@
1
+ # src/bsm_cli/account.py
2
+ """CLI commands for account management."""
3
+
4
+ import click
5
+ from bsm_cli.decorators import pass_async_context
6
+
7
+
8
+ @click.group()
9
+ def account():
10
+ """Commands for managing your account."""
11
+ pass
12
+
13
+
14
+ @account.command()
15
+ @pass_async_context
16
+ async def details(ctx):
17
+ """Get your account details."""
18
+ client = ctx.obj["client"]
19
+ details = await client.async_get_account_details()
20
+ click.echo(details.model_dump_json(indent=2))
21
+
22
+
23
+ @account.command()
24
+ @click.option("--theme", prompt="Theme name", help="The name of the theme to set.")
25
+ @pass_async_context
26
+ async def update_theme(ctx, theme):
27
+ """Update your theme."""
28
+ from bsm_api_client.models import ThemeUpdatePayload
29
+
30
+ client = ctx.obj["client"]
31
+ payload = ThemeUpdatePayload(theme=theme)
32
+ response = await client.async_update_theme(payload)
33
+ click.echo(response.model_dump_json(indent=2))
34
+
35
+
36
+ @account.command()
37
+ @click.option("--full-name", prompt="Full Name", help="Your full name.")
38
+ @click.option("--email", prompt="Email", help="Your email address.")
39
+ @pass_async_context
40
+ async def update_profile(ctx, full_name, email):
41
+ """Update your profile."""
42
+ from bsm_api_client.models import ProfileUpdatePayload
43
+
44
+ client = ctx.obj["client"]
45
+ payload = ProfileUpdatePayload(full_name=full_name, email=email)
46
+ response = await client.async_update_profile(payload)
47
+ click.echo(response.model_dump_json(indent=2))
48
+
49
+
50
+ @account.command()
51
+ @click.option(
52
+ "--current-password",
53
+ prompt=True,
54
+ hide_input=True,
55
+ confirmation_prompt=False,
56
+ help="Your current password.",
57
+ )
58
+ @click.option(
59
+ "--new-password",
60
+ prompt=True,
61
+ hide_input=True,
62
+ confirmation_prompt=True,
63
+ help="Your new password.",
64
+ )
65
+ @pass_async_context
66
+ async def change_password(ctx, current_password, new_password):
67
+ """Change your password."""
68
+ from bsm_api_client.models import ChangePasswordPayload
69
+
70
+ client = ctx.obj["client"]
71
+ payload = ChangePasswordPayload(
72
+ current_password=current_password, new_password=new_password
73
+ )
74
+ response = await client.async_change_password(payload)
75
+ click.echo(response.model_dump_json(indent=2))
bsm_cli/addon.py ADDED
@@ -0,0 +1,362 @@
1
+ import os
2
+
3
+ import click
4
+ import questionary
5
+ from bsm_cli.decorators import monitor_task, pass_async_context
6
+ from questionary import Separator
7
+
8
+ from bsm_api_client.models import (
9
+ AddonActionPayload,
10
+ AddonReorderPayload,
11
+ AddonSubpackPayload,
12
+ FileNamePayload,
13
+ )
14
+
15
+
16
+ @click.group()
17
+ def addon():
18
+ """Manages server addons."""
19
+ pass
20
+
21
+
22
+ @addon.command("install")
23
+ @click.option(
24
+ "-s", "--server", "server_name", required=True, help="Name of the target server."
25
+ )
26
+ @click.option(
27
+ "-f",
28
+ "--file",
29
+ "addon_file_path",
30
+ type=click.Path(exists=True, dir_okay=False, resolve_path=True),
31
+ help="Path to the addon file (.mcpack, .mcaddon); skips interactive menu.",
32
+ )
33
+ @pass_async_context
34
+ async def install_addon(ctx, server_name: str, addon_file_path: str):
35
+ """Installs a behavior or resource pack addon to a specified server."""
36
+ client = ctx.obj.get("client")
37
+ if not client:
38
+ click.secho("You are not logged in.", fg="red")
39
+ return
40
+
41
+ try:
42
+ selected_addon_path = addon_file_path
43
+
44
+ if not selected_addon_path:
45
+ click.secho(
46
+ f"Entering interactive addon installation for server: {server_name}",
47
+ fg="yellow",
48
+ )
49
+ response = await client.async_get_content_addons()
50
+ available_files = response.files
51
+
52
+ if not available_files:
53
+ click.secho(
54
+ "No addon files found in the content/addons directory. Nothing to install.",
55
+ fg="yellow",
56
+ )
57
+ return
58
+
59
+ file_map = {os.path.basename(f): f for f in available_files}
60
+ choices = sorted(list(file_map.keys())) + ["Cancel"]
61
+ selection = await questionary.select(
62
+ "Select an addon to install:", choices=choices
63
+ ).ask_async()
64
+
65
+ if not selection or selection == "Cancel":
66
+ raise click.Abort()
67
+ selected_addon_path = file_map[selection]
68
+
69
+ addon_filename = os.path.basename(selected_addon_path)
70
+ click.echo(f"Installing addon '{addon_filename}' to server '{server_name}'...")
71
+
72
+ payload = FileNamePayload(filename=addon_filename)
73
+ response = await client.async_install_server_addon(server_name, payload)
74
+ if response.task_id:
75
+ await monitor_task(
76
+ client,
77
+ response.task_id,
78
+ f"Addon '{addon_filename}' installed successfully",
79
+ "Failed to install addon",
80
+ )
81
+ elif response.status == "success":
82
+ click.secho(f"Addon '{addon_filename}' installed successfully.", fg="green")
83
+ else:
84
+ click.secho(f"Failed to install addon: {response.message}", fg="red")
85
+
86
+ except Exception as e:
87
+ click.secho(f"An error occurred: {e}", fg="red")
88
+
89
+
90
+ @addon.command("manage")
91
+ @click.option(
92
+ "-s", "--server", "server_name", required=True, help="Name of the target server."
93
+ )
94
+ @pass_async_context
95
+ async def manage_addons(ctx, server_name: str): # noqa: C901
96
+ """Interactively manages installed addons on a specified server."""
97
+ client = ctx.obj.get("client")
98
+ if not client:
99
+ click.secho("You are not logged in.", fg="red")
100
+ return
101
+
102
+ while True:
103
+ try:
104
+ response = await client.async_get_server_addons(server_name)
105
+ addons = response.addons
106
+ if not addons:
107
+ click.secho(
108
+ "Failed to fetch installed addons or no addons returned.", fg="red"
109
+ )
110
+ return
111
+
112
+ bp = addons.behavior_packs or []
113
+ rp = addons.resource_packs or []
114
+
115
+ click.clear()
116
+ click.secho(
117
+ f"--- Manage Addons for {server_name} ---", fg="magenta", bold=True
118
+ )
119
+
120
+ from questionary import Choice
121
+
122
+ menu_choices: list[Choice | Separator | str] = [
123
+ Separator("--- Behavior Packs ---")
124
+ ]
125
+
126
+ for idx, pack in enumerate(bp, 1):
127
+ status = "🟢" if pack.status == "ACTIVE" else "⚪"
128
+ menu_choices.append(
129
+ Choice(
130
+ f"{idx}. {status} [BP] {pack.name} (v{'.'.join(map(str, pack.version))})",
131
+ value=f"{idx}. {status} [BP] {pack.name} (v{'.'.join(map(str, pack.version))})",
132
+ )
133
+ )
134
+
135
+ menu_choices.append(Separator("--- Resource Packs ---"))
136
+ for idx, pack in enumerate(rp, 1):
137
+ status = "🟢" if pack.status == "ACTIVE" else "⚪"
138
+ menu_choices.append(
139
+ Choice(
140
+ f"{idx}. {status} [RP] {pack.name} (v{'.'.join(map(str, pack.version))})",
141
+ value=f"{idx}. {status} [RP] {pack.name} (v{'.'.join(map(str, pack.version))})",
142
+ )
143
+ )
144
+
145
+ menu_choices.extend(
146
+ [
147
+ Separator("--- Actions ---"),
148
+ Choice("Reorder Behavior Packs", value="Reorder Behavior Packs"),
149
+ Choice("Reorder Resource Packs", value="Reorder Resource Packs"),
150
+ Choice("Back", value="Back"),
151
+ ]
152
+ )
153
+
154
+ choice = await questionary.select(
155
+ "Select an addon to manage or an action:", choices=menu_choices
156
+ ).ask_async()
157
+
158
+ if not choice or choice == "Back":
159
+ return
160
+
161
+ if choice == "Reorder Behavior Packs":
162
+ active_bp = [p for p in bp if p.status == "ACTIVE"]
163
+ if not active_bp:
164
+ click.secho("No active behavior packs to reorder.", fg="yellow")
165
+ await questionary.press_any_key_to_continue().ask_async()
166
+ continue
167
+
168
+ from typing import List
169
+
170
+ ordered_uuids: List[str] = []
171
+ remaining_bp = list(active_bp)
172
+
173
+ while remaining_bp:
174
+ choices = [f"{p.name} ({p.uuid})" for p in remaining_bp]
175
+ selected_str = await questionary.select(
176
+ f"Select pack for position {len(ordered_uuids) + 1}:",
177
+ choices=choices,
178
+ ).ask_async()
179
+ if not selected_str:
180
+ break
181
+
182
+ uuid = selected_str.split("(")[-1].replace(")", "")
183
+ ordered_uuids.append(uuid)
184
+ remaining_bp = [p for p in remaining_bp if p.uuid != uuid]
185
+
186
+ if len(ordered_uuids) == len(active_bp):
187
+ payload = AddonReorderPayload(
188
+ pack_type="behavior", uuids=ordered_uuids
189
+ )
190
+ res = await client.async_reorder_server_addon(server_name, payload)
191
+ if res.task_id:
192
+ await monitor_task(
193
+ client,
194
+ res.task_id,
195
+ "Reorder successful",
196
+ "Failed to reorder",
197
+ )
198
+ else:
199
+ click.secho("Reordered successfully.", fg="green")
200
+ else:
201
+ click.secho("Reorder cancelled.", fg="yellow")
202
+
203
+ await questionary.press_any_key_to_continue().ask_async()
204
+ continue
205
+
206
+ if choice == "Reorder Resource Packs":
207
+ active_rp = [p for p in rp if p.status == "ACTIVE"]
208
+ if not active_rp:
209
+ click.secho("No active resource packs to reorder.", fg="yellow")
210
+ await questionary.press_any_key_to_continue().ask_async()
211
+ continue
212
+
213
+ ordered_uuids = []
214
+ remaining_rp = list(active_rp)
215
+
216
+ while remaining_rp:
217
+ choices = [f"{p.name} ({p.uuid})" for p in remaining_rp]
218
+ selected_str = await questionary.select(
219
+ f"Select pack for position {len(ordered_uuids) + 1}:",
220
+ choices=choices,
221
+ ).ask_async()
222
+ if not selected_str:
223
+ break
224
+
225
+ uuid = selected_str.split("(")[-1].replace(")", "")
226
+ ordered_uuids.append(uuid)
227
+ remaining_rp = [p for p in remaining_rp if p.uuid != uuid]
228
+
229
+ if len(ordered_uuids) == len(active_rp):
230
+ payload = AddonReorderPayload(
231
+ pack_type="resource", uuids=ordered_uuids
232
+ )
233
+ res = await client.async_reorder_server_addon(server_name, payload)
234
+ if res.task_id:
235
+ await monitor_task(
236
+ client,
237
+ res.task_id,
238
+ "Reorder successful",
239
+ "Failed to reorder",
240
+ )
241
+ else:
242
+ click.secho("Reordered successfully.", fg="green")
243
+ else:
244
+ click.secho("Reorder cancelled.", fg="yellow")
245
+
246
+ await questionary.press_any_key_to_continue().ask_async()
247
+ continue
248
+
249
+ # Handle specific pack
250
+ is_bp = "[BP]" in choice
251
+ pack_type = "behavior" if is_bp else "resource"
252
+ pack_name = choice.split("] ")[1].split(" (v")[0]
253
+
254
+ pack = next((p for p in (bp if is_bp else rp) if p.name == pack_name), None)
255
+
256
+ if not pack:
257
+ continue
258
+
259
+ pack_menu = []
260
+ if pack.status == "ACTIVE":
261
+ pack_menu.append("Disable")
262
+ if pack.subpacks:
263
+ pack_menu.append("Change Subpack")
264
+ else:
265
+ pack_menu.append("Enable")
266
+
267
+ pack_menu.extend(["Uninstall", "Back"])
268
+
269
+ action_choice = await questionary.select(
270
+ f"Actions for {pack.name}:", choices=pack_menu
271
+ ).ask_async()
272
+
273
+ if not action_choice or action_choice == "Back":
274
+ continue
275
+
276
+ if action_choice == "Enable":
277
+ res = await client.async_enable_server_addon(
278
+ server_name,
279
+ AddonActionPayload(pack_uuid=pack.uuid, pack_type=pack_type),
280
+ )
281
+ if res.task_id:
282
+ await monitor_task(
283
+ client, res.task_id, "Enabled successfully", "Failed to enable"
284
+ )
285
+ elif action_choice == "Disable":
286
+ res = await client.async_disable_server_addon(
287
+ server_name,
288
+ AddonActionPayload(pack_uuid=pack.uuid, pack_type=pack_type),
289
+ )
290
+ if res.task_id:
291
+ await monitor_task(
292
+ client,
293
+ res.task_id,
294
+ "Disabled successfully",
295
+ "Failed to disable",
296
+ )
297
+ elif action_choice == "Uninstall":
298
+ if await questionary.confirm(
299
+ f"Are you sure you want to uninstall {pack.name}?"
300
+ ).ask_async():
301
+ res = await client.async_uninstall_server_addon(
302
+ server_name,
303
+ AddonActionPayload(pack_uuid=pack.uuid, pack_type=pack_type),
304
+ )
305
+ if res.task_id:
306
+ await monitor_task(
307
+ client,
308
+ res.task_id,
309
+ "Uninstalled successfully",
310
+ "Failed to uninstall",
311
+ )
312
+ elif action_choice == "Change Subpack":
313
+ subpack_choices = []
314
+ default_sp = None
315
+ for sp in pack.subpacks:
316
+ sp_name = sp.get("name") or sp.get("folder_name")
317
+ if pack.active_subpack == sp.get("folder_name"):
318
+ sp_name += " (Active)"
319
+ default_sp = sp_name
320
+ subpack_choices.append(sp_name)
321
+
322
+ sp_choice = await questionary.select(
323
+ "Select new subpack:", choices=subpack_choices, default=default_sp
324
+ ).ask_async()
325
+
326
+ if sp_choice:
327
+ clean_sp_choice = sp_choice.replace(" (Active)", "")
328
+ selected_sp = next(
329
+ (
330
+ sp
331
+ for sp in pack.subpacks
332
+ if sp.get("name") == clean_sp_choice
333
+ or sp.get("folder_name") == clean_sp_choice
334
+ ),
335
+ None,
336
+ )
337
+ if selected_sp:
338
+ subpack_payload = AddonSubpackPayload(
339
+ pack_uuid=pack.uuid,
340
+ pack_type=pack_type,
341
+ subpack_name=selected_sp.get("folder_name"),
342
+ )
343
+ setattr(
344
+ subpack_payload,
345
+ f"subpack_{pack.uuid}",
346
+ selected_sp.get("folder_name"),
347
+ )
348
+
349
+ res = await client.async_update_server_addon_subpack(
350
+ server_name, subpack_payload
351
+ )
352
+ if res.task_id:
353
+ await monitor_task(
354
+ client,
355
+ res.task_id,
356
+ "Subpack updated successfully",
357
+ "Failed to update subpack",
358
+ )
359
+
360
+ except Exception as e:
361
+ click.secho(f"An error occurred: {e}", fg="red")
362
+ await questionary.press_any_key_to_continue().ask_async()