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/plugins.py ADDED
@@ -0,0 +1,296 @@
1
+ import json
2
+
3
+ import click
4
+ import questionary
5
+
6
+ from bsm_api_client.models import PluginStatusSetPayload, TriggerEventPayload
7
+
8
+
9
+ def _print_plugin_table(plugins):
10
+ """
11
+ Internal helper to print a formatted table of plugins, their statuses, and versions.
12
+ """
13
+ if not plugins:
14
+ click.secho("No plugins found or configured.", fg="yellow")
15
+ return
16
+
17
+ click.secho("BSM API Client - Plugin Statuses & Versions", fg="magenta", bold=True)
18
+
19
+ plugin_names = list(plugins.keys())
20
+ versions = [config.get("version", "N/A") for config in plugins.values()]
21
+
22
+ max_name_len = max(len(name) for name in plugin_names) if plugin_names else 20
23
+ max_version_len = max(
24
+ (max(len(v) for v in versions) if versions else 0), len("Version")
25
+ )
26
+ max_status_len = len("Disabled")
27
+
28
+ header = f"{'Plugin Name':<{max_name_len}} | {'Status':<{max_status_len}} | {'Version':<{max_version_len}}"
29
+ click.secho(header, underline=True)
30
+ click.secho("-" * len(header))
31
+
32
+ for name, config in sorted(plugins.items()):
33
+ is_enabled = config.get("enabled", False)
34
+ version = config.get("version", "N/A")
35
+
36
+ status_str = "Enabled" if is_enabled else "Disabled"
37
+ status_color = "green" if is_enabled else "red"
38
+
39
+ click.echo(f"{name:<{max_name_len}} | ", nl=False)
40
+ click.secho(f"{status_str:<{max_status_len}}", fg=status_color, nl=False)
41
+ click.echo(f" | {version:<{max_version_len}}")
42
+
43
+
44
+ async def interactive_plugin_workflow(client): # noqa: C901
45
+ """Guides the user through an interactive session to enable or disable plugins."""
46
+ try:
47
+ response = await client.async_get_plugin_statuses()
48
+ if response.status != "success":
49
+ click.secho(
50
+ f"Failed to retrieve plugin statuses: {response.message}", fg="red"
51
+ )
52
+ return
53
+
54
+ plugins = response.plugins
55
+ if not plugins:
56
+ click.secho("No plugins found or configured to edit.", fg="yellow")
57
+ return
58
+
59
+ _print_plugin_table(plugins)
60
+ click.echo()
61
+
62
+ while True:
63
+ click.clear()
64
+ click.secho("--- Manage Plugins ---", fg="magenta", bold=True)
65
+
66
+ response = await client.async_get_plugin_statuses()
67
+ if response.status != "success":
68
+ click.secho(
69
+ f"Failed to retrieve plugin statuses: {response.message}", fg="red"
70
+ )
71
+ return
72
+
73
+ plugins = response.plugins
74
+ if not plugins:
75
+ click.secho("No plugins found or configured to edit.", fg="yellow")
76
+ return
77
+
78
+ menu_choices = []
79
+ for name, config_dict in sorted(plugins.items()):
80
+ is_enabled = config_dict.get("enabled", False)
81
+ version = config_dict.get("version", "N/A")
82
+ status = "🟢" if is_enabled else "⚪"
83
+ menu_choices.append(
84
+ questionary.Choice(
85
+ title=f"{status} {name} (v{version})", value=name
86
+ )
87
+ )
88
+
89
+ menu_choices.extend(
90
+ [
91
+ questionary.Separator("--- Actions ---"),
92
+ questionary.Choice(title="Reload All Plugins", value="RELOAD"),
93
+ questionary.Choice(title="Back", value="BACK"),
94
+ ]
95
+ )
96
+
97
+ choice = await questionary.select(
98
+ "Select a plugin to manage or an action:", choices=menu_choices
99
+ ).ask_async()
100
+
101
+ if not choice or choice == "BACK":
102
+ return
103
+
104
+ if choice == "RELOAD":
105
+ click.secho("Reloading plugins...", fg="cyan")
106
+ try:
107
+ reload_response = await client.async_reload_plugins()
108
+ if reload_response.status == "success":
109
+ click.secho(reload_response.message, fg="green")
110
+ else:
111
+ click.secho(
112
+ f"Failed to reload plugins: {reload_response.message}",
113
+ fg="red",
114
+ )
115
+ except Exception as e_reload:
116
+ click.secho(f"Error reloading plugins: {e_reload}", fg="red")
117
+ click.pause()
118
+ continue
119
+
120
+ # Handle specific plugin
121
+ plugin_name = choice
122
+ config_dict = plugins.get(plugin_name)
123
+
124
+ if not config_dict:
125
+ continue
126
+
127
+ is_enabled = config_dict.get("enabled", False)
128
+ pack_menu = []
129
+ if is_enabled:
130
+ pack_menu.append("Disable")
131
+ else:
132
+ pack_menu.append("Enable")
133
+
134
+ pack_menu.append("Back")
135
+
136
+ action_choice = await questionary.select(
137
+ f"Actions for {plugin_name}:", choices=pack_menu
138
+ ).ask_async()
139
+
140
+ if not action_choice or action_choice == "Back":
141
+ continue
142
+
143
+ if action_choice == "Enable":
144
+ payload = PluginStatusSetPayload(enabled=True)
145
+ res = await client.async_set_plugin_status(plugin_name, payload)
146
+ if res.status == "success":
147
+ click.secho(
148
+ f"Plugin '{plugin_name}' enabled successfully.", fg="green"
149
+ )
150
+ else:
151
+ click.secho(
152
+ f"Failed to enable plugin '{plugin_name}': {res.message}",
153
+ fg="red",
154
+ )
155
+ elif action_choice == "Disable":
156
+ payload = PluginStatusSetPayload(enabled=False)
157
+ res = await client.async_set_plugin_status(plugin_name, payload)
158
+ if res.status == "success":
159
+ click.secho(
160
+ f"Plugin '{plugin_name}' disabled successfully.", fg="green"
161
+ )
162
+ else:
163
+ click.secho(
164
+ f"Failed to disable plugin '{plugin_name}': {res.message}",
165
+ fg="red",
166
+ )
167
+
168
+ click.pause()
169
+
170
+ except Exception as e:
171
+ click.secho(f"An error occurred during plugin configuration: {e}", fg="red")
172
+
173
+
174
+ @click.group(invoke_without_command=True)
175
+ @click.pass_context
176
+ async def plugin(ctx):
177
+ """Manages plugins."""
178
+ if ctx.invoked_subcommand is None:
179
+ client = ctx.obj.get("client")
180
+ if not client:
181
+ click.secho("You are not logged in.", fg="red")
182
+ return
183
+ await interactive_plugin_workflow(client)
184
+
185
+
186
+ @plugin.command("list")
187
+ @click.pass_context
188
+ async def list_plugins(ctx):
189
+ """Lists all discoverable plugins."""
190
+ client = ctx.obj.get("client")
191
+ if not client:
192
+ click.secho("You are not logged in.", fg="red")
193
+ return
194
+
195
+ try:
196
+ response = await client.async_get_plugin_statuses()
197
+ if response.status == "success":
198
+ plugins = response.plugins
199
+ if not plugins:
200
+ click.secho("No plugins found.", fg="yellow")
201
+ return
202
+
203
+ _print_plugin_table(plugins)
204
+ else:
205
+ click.secho(f"Failed to list plugins: {response.message}", fg="red")
206
+ except Exception as e:
207
+ click.secho(f"An error occurred: {e}", fg="red")
208
+
209
+
210
+ @plugin.command("enable")
211
+ @click.argument("plugin_name")
212
+ @click.pass_context
213
+ async def enable_plugin(ctx, plugin_name: str):
214
+ """Enables a plugin."""
215
+ client = ctx.obj.get("client")
216
+ if not client:
217
+ click.secho("You are not logged in.", fg="red")
218
+ return
219
+
220
+ try:
221
+ payload = PluginStatusSetPayload(enabled=True)
222
+ response = await client.async_set_plugin_status(plugin_name, payload)
223
+ if response.status == "success":
224
+ click.secho(f"Plugin '{plugin_name}' enabled successfully.", fg="green")
225
+ else:
226
+ click.secho(f"Failed to enable plugin: {response.message}", fg="red")
227
+ except Exception as e:
228
+ click.secho(f"An error occurred: {e}", fg="red")
229
+
230
+
231
+ @plugin.command("disable")
232
+ @click.argument("plugin_name")
233
+ @click.pass_context
234
+ async def disable_plugin(ctx, plugin_name: str):
235
+ """Disables a plugin."""
236
+ client = ctx.obj.get("client")
237
+ if not client:
238
+ click.secho("You are not logged in.", fg="red")
239
+ return
240
+
241
+ try:
242
+ payload = PluginStatusSetPayload(enabled=False)
243
+ response = await client.async_set_plugin_status(plugin_name, payload)
244
+ if response.status == "success":
245
+ click.secho(f"Plugin '{plugin_name}' disabled successfully.", fg="green")
246
+ else:
247
+ click.secho(f"Failed to disable plugin: {response.message}", fg="red")
248
+ except Exception as e:
249
+ click.secho(f"An error occurred: {e}", fg="red")
250
+
251
+
252
+ @plugin.command("reload")
253
+ @click.pass_context
254
+ async def reload_plugins(ctx):
255
+ """Reloads all plugins."""
256
+ client = ctx.obj.get("client")
257
+ if not client:
258
+ click.secho("You are not logged in.", fg="red")
259
+ return
260
+
261
+ try:
262
+ response = await client.async_reload_plugins()
263
+ if response.status == "success":
264
+ click.secho("Plugins reloaded successfully.", fg="green")
265
+ else:
266
+ click.secho(f"Failed to reload plugins: {response.message}", fg="red")
267
+ except Exception as e:
268
+ click.secho(f"An error occurred: {e}", fg="red")
269
+
270
+
271
+ @plugin.command("trigger-event")
272
+ @click.argument("event_name")
273
+ @click.option(
274
+ "--payload-json", help="Optional JSON string to use as the event payload."
275
+ )
276
+ @click.pass_context
277
+ async def trigger_event(ctx, event_name: str, payload_json: str):
278
+ """Triggers a custom plugin event."""
279
+ client = ctx.obj.get("client")
280
+ if not client:
281
+ click.secho("You are not logged in.", fg="red")
282
+ return
283
+
284
+ try:
285
+ payload = None
286
+ if payload_json:
287
+ payload = json.loads(payload_json)
288
+
289
+ event_payload = TriggerEventPayload(event_name=event_name, payload=payload)
290
+ response = await client.async_trigger_plugin_event(event_payload)
291
+ if response.status == "success":
292
+ click.secho(f"Event '{event_name}' triggered successfully.", fg="green")
293
+ else:
294
+ click.secho(f"Failed to trigger event: {response.message}", fg="red")
295
+ except Exception as e:
296
+ click.secho(f"An error occurred: {e}", fg="red")
bsm_cli/properties.py ADDED
@@ -0,0 +1,197 @@
1
+ import click
2
+ import questionary
3
+
4
+ from bsm_api_client.models import PropertiesPayload
5
+
6
+
7
+ @click.group()
8
+ def properties():
9
+ """Manages a server's server.properties file."""
10
+ pass
11
+
12
+
13
+ @properties.command("get")
14
+ @click.option(
15
+ "-s",
16
+ "--server",
17
+ "server_name",
18
+ required=True,
19
+ help="The name of the target server.",
20
+ )
21
+ @click.option("-p", "--prop", "property_name", help="Display a single property value.")
22
+ @click.pass_context
23
+ async def get_props(ctx, server_name: str, property_name: str):
24
+ """Displays server properties from a server's server.properties file."""
25
+ client = ctx.obj.get("client")
26
+ if not client:
27
+ click.secho("You are not logged in.", fg="red")
28
+ return
29
+
30
+ response = await client.async_get_server_properties(server_name)
31
+
32
+ if response.status == "success":
33
+ properties = response.properties
34
+ if property_name:
35
+ value = properties.get(property_name)
36
+ if value is not None:
37
+ click.echo(value)
38
+ else:
39
+ click.secho(f"Error: Property '{property_name}' not found.", fg="red")
40
+ else:
41
+ click.secho(f"\nProperties for '{server_name}':", bold=True)
42
+ max_key_len = max(len(k) for k in properties.keys()) if properties else 0
43
+ for key, value in sorted(properties.items()):
44
+ click.echo(f" {key:<{max_key_len}} = {value}")
45
+ else:
46
+ click.secho(f"Failed to get properties: {response.message}", fg="red")
47
+
48
+
49
+ @properties.command("set")
50
+ @click.option(
51
+ "-s",
52
+ "--server",
53
+ "server_name",
54
+ required=True,
55
+ help="The name of the target server.",
56
+ )
57
+ @click.option(
58
+ "-p",
59
+ "--prop",
60
+ "properties",
61
+ multiple=True,
62
+ help="A 'key=value' pair to set. Use multiple times for multiple properties.",
63
+ )
64
+ @click.pass_context
65
+ async def set_props(ctx, server_name: str, properties: tuple[str]):
66
+ """Sets one or more properties in a server's server.properties file."""
67
+ client = ctx.obj.get("client")
68
+ if not client:
69
+ click.secho("You are not logged in.", fg="red")
70
+ return
71
+
72
+ try:
73
+ if not properties:
74
+ click.secho(
75
+ f"No properties specified; starting interactive editor for '{server_name}'...",
76
+ fg="yellow",
77
+ )
78
+ await interactive_properties_workflow(client, server_name)
79
+ return
80
+
81
+ props_to_update = {}
82
+ for p in properties:
83
+ if "=" not in p:
84
+ click.secho(f"Error: Invalid format '{p}'. Use 'key=value'.", fg="red")
85
+ raise click.Abort()
86
+ key, value = p.split("=", 1)
87
+ props_to_update[key.strip()] = value.strip()
88
+
89
+ click.echo(
90
+ f"Updating {len(props_to_update)} propert(y/ies) for '{server_name}'..."
91
+ )
92
+
93
+ payload = PropertiesPayload(properties=props_to_update)
94
+ response = await client.async_update_server_properties(server_name, payload)
95
+
96
+ if response.status == "success":
97
+ click.secho("Properties updated successfully.", fg="green")
98
+ else:
99
+ click.secho(f"Failed to set properties: {response.message}", fg="red")
100
+
101
+ except Exception as e:
102
+ click.secho(f"An error occurred: {e}", fg="red")
103
+
104
+
105
+ async def interactive_properties_workflow(client, server_name: str): # noqa: C901
106
+ """Guides a user through an interactive session to edit `server.properties`."""
107
+ click.secho("\n--- Interactive Server Properties Configuration ---", bold=True)
108
+ click.echo("Loading current server properties...")
109
+
110
+ properties_response = await client.async_get_server_properties(server_name)
111
+ if properties_response.status == "error":
112
+ click.secho(f"Error: {properties_response.message}", fg="red")
113
+ raise click.Abort()
114
+
115
+ current_properties = (
116
+ properties_response.properties if properties_response.properties else {}
117
+ )
118
+ changes = {}
119
+
120
+ async def _prompt(prop: str, message: str, prompter, **kwargs):
121
+ """A nested helper to abstract the prompting and change-tracking logic."""
122
+ original_value = current_properties.get(prop)
123
+
124
+ if prompter == questionary.confirm:
125
+ default_bool = str(original_value).lower() == "true"
126
+ new_val = await prompter(
127
+ message, default=default_bool, **kwargs
128
+ ).ask_async()
129
+ if new_val is None:
130
+ return
131
+ if new_val != default_bool:
132
+ changes[prop] = str(new_val).lower()
133
+ else:
134
+ new_val = await prompter(
135
+ message, default=str(original_value), **kwargs
136
+ ).ask_async()
137
+ if new_val is None:
138
+ return
139
+ if new_val != original_value:
140
+ changes[prop] = new_val
141
+
142
+ await _prompt("server-name", "Server name (visible in LAN list):", questionary.text)
143
+ await _prompt("level-name", "World folder name:", questionary.text)
144
+ await _prompt(
145
+ "gamemode",
146
+ "Default gamemode:",
147
+ questionary.select,
148
+ choices=["survival", "creative", "adventure"],
149
+ )
150
+ await _prompt(
151
+ "difficulty",
152
+ "Game difficulty:",
153
+ questionary.select,
154
+ choices=["peaceful", "easy", "normal", "hard"],
155
+ )
156
+ await _prompt("allow-cheats", "Allow cheats:", questionary.confirm)
157
+ await _prompt("max-players", "Maximum players:", questionary.text)
158
+ await _prompt(
159
+ "online-mode", "Require Xbox Live authentication:", questionary.confirm
160
+ )
161
+ await _prompt("allow-list", "Enable allowlist:", questionary.confirm)
162
+ await _prompt(
163
+ "default-player-permission-level",
164
+ "Default permission for new players:",
165
+ questionary.select,
166
+ choices=["visitor", "member", "operator"],
167
+ )
168
+ await _prompt("view-distance", "View distance (chunks):", questionary.text)
169
+ await _prompt(
170
+ "tick-distance", "Tick simulation distance (chunks):", questionary.text
171
+ )
172
+ await _prompt(
173
+ "level-seed", "Level seed (leave blank for random):", questionary.text
174
+ )
175
+ await _prompt("texturepack-required", "Require texture packs:", questionary.confirm)
176
+
177
+ if not changes:
178
+ click.secho("\nNo properties were changed.", fg="cyan")
179
+ return
180
+
181
+ click.secho("\nApplying the following changes:", bold=True)
182
+ for key, value in changes.items():
183
+ original = current_properties.get(key, "not set")
184
+ click.echo(
185
+ f" - {key}: {click.style(original, fg='red')} -> {click.style(value, fg='green')}"
186
+ )
187
+
188
+ if not await questionary.confirm("Save these changes?", default=True).ask_async():
189
+ raise click.Abort()
190
+
191
+ payload = PropertiesPayload(properties=changes)
192
+ update_response = await client.async_update_server_properties(server_name, payload)
193
+
194
+ if update_response.status == "success":
195
+ click.secho("Server properties updated successfully.", fg="green")
196
+ else:
197
+ click.secho(f"Failed to update properties: {update_response.message}", fg="red")