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/users.py ADDED
@@ -0,0 +1,245 @@
1
+ import click
2
+ import questionary
3
+ from bsm_cli.decorators import pass_async_context
4
+
5
+
6
+ async def interactive_user_workflow(ctx, client): # noqa: C901
7
+ """Interactive menu for managing users."""
8
+ while True:
9
+ try:
10
+ users_list = await client.async_get_users()
11
+ click.clear()
12
+ click.secho("--- Manage Users ---", fg="magenta", bold=True)
13
+ for user in users_list:
14
+ click.echo(
15
+ f"ID: {user.id} | Username: {user.username} | Role: {user.role} | Active: {user.is_active} | Type: {user.identity_type}"
16
+ )
17
+
18
+ choices = [
19
+ "List Users",
20
+ "Delete User",
21
+ "Set User Role",
22
+ "Enable User",
23
+ "Disable User",
24
+ "Generate Invite Link",
25
+ "Back",
26
+ ]
27
+ choice = await questionary.select(
28
+ "\nChoose an action:",
29
+ choices=choices,
30
+ use_indicator=True,
31
+ ).ask_async()
32
+
33
+ if choice is None or choice == "Back":
34
+ return
35
+
36
+ if choice == "List Users":
37
+ click.echo("\nUsers List:")
38
+ for user in users_list:
39
+ click.echo(
40
+ f"ID: {user.id} | Username: {user.username} | Role: {user.role} | Active: {user.is_active} | Type: {user.identity_type}"
41
+ )
42
+ await questionary.press_any_key_to_continue().ask_async()
43
+
44
+ elif choice == "Delete User":
45
+ if not users_list:
46
+ click.secho("No users found.", fg="yellow")
47
+ await questionary.press_any_key_to_continue().ask_async()
48
+ continue
49
+
50
+ user_choices = [
51
+ questionary.Choice(title=f"{u.username} (ID: {u.id})", value=u.id)
52
+ for u in users_list
53
+ ]
54
+ user_choices.append(questionary.Choice(title="Cancel", value="Cancel"))
55
+
56
+ user_id = await questionary.select(
57
+ "Select user to delete:", choices=user_choices
58
+ ).ask_async()
59
+
60
+ if user_id and user_id != "Cancel":
61
+ confirm = await questionary.confirm(
62
+ f"Are you sure you want to delete user {user_id}?"
63
+ ).ask_async()
64
+ if confirm:
65
+ response = await client.async_delete_user(user_id)
66
+ click.echo(response.model_dump_json(indent=2))
67
+ await questionary.press_any_key_to_continue().ask_async()
68
+
69
+ elif choice == "Set User Role":
70
+ if not users_list:
71
+ click.secho("No users found.", fg="yellow")
72
+ await questionary.press_any_key_to_continue().ask_async()
73
+ continue
74
+
75
+ user_choices = [
76
+ questionary.Choice(title=f"{u.username} (ID: {u.id})", value=u.id)
77
+ for u in users_list
78
+ ]
79
+ user_choices.append(questionary.Choice(title="Cancel", value="Cancel"))
80
+
81
+ user_id = await questionary.select(
82
+ "Select user to modify:", choices=user_choices
83
+ ).ask_async()
84
+
85
+ if user_id and user_id != "Cancel":
86
+ role = await questionary.select(
87
+ "Select new role:",
88
+ choices=["user", "moderator", "admin"],
89
+ ).ask_async()
90
+ if role:
91
+ response = await client.async_update_user_role(user_id, role)
92
+ click.echo(response.model_dump_json(indent=2))
93
+ await questionary.press_any_key_to_continue().ask_async()
94
+
95
+ elif choice == "Enable User":
96
+ if not users_list:
97
+ click.secho("No users found.", fg="yellow")
98
+ await questionary.press_any_key_to_continue().ask_async()
99
+ continue
100
+
101
+ disabled_users = [u for u in users_list if not u.is_active]
102
+ if not disabled_users:
103
+ click.secho("No disabled users found.", fg="yellow")
104
+ await questionary.press_any_key_to_continue().ask_async()
105
+ continue
106
+
107
+ user_choices = [
108
+ questionary.Choice(title=f"{u.username} (ID: {u.id})", value=u.id)
109
+ for u in disabled_users
110
+ ]
111
+ user_choices.append(questionary.Choice(title="Cancel", value="Cancel"))
112
+
113
+ user_id = await questionary.select(
114
+ "Select user to enable:", choices=user_choices
115
+ ).ask_async()
116
+
117
+ if user_id and user_id != "Cancel":
118
+ response = await client.async_enable_user(user_id)
119
+ click.echo(response.model_dump_json(indent=2))
120
+ await questionary.press_any_key_to_continue().ask_async()
121
+
122
+ elif choice == "Disable User":
123
+ if not users_list:
124
+ click.secho("No users found.", fg="yellow")
125
+ await questionary.press_any_key_to_continue().ask_async()
126
+ continue
127
+
128
+ enabled_users = [u for u in users_list if u.is_active]
129
+ if not enabled_users:
130
+ click.secho("No enabled users found.", fg="yellow")
131
+ await questionary.press_any_key_to_continue().ask_async()
132
+ continue
133
+
134
+ user_choices = [
135
+ questionary.Choice(title=f"{u.username} (ID: {u.id})", value=u.id)
136
+ for u in enabled_users
137
+ ]
138
+ user_choices.append(questionary.Choice(title="Cancel", value="Cancel"))
139
+
140
+ user_id = await questionary.select(
141
+ "Select user to disable:", choices=user_choices
142
+ ).ask_async()
143
+
144
+ if user_id and user_id != "Cancel":
145
+ response = await client.async_disable_user(user_id)
146
+ click.echo(response.model_dump_json(indent=2))
147
+ await questionary.press_any_key_to_continue().ask_async()
148
+
149
+ elif choice == "Generate Invite Link":
150
+ role = await questionary.select(
151
+ "Select role for invite:",
152
+ choices=["user", "moderator", "admin"],
153
+ ).ask_async()
154
+ if role:
155
+ response = await client.async_generate_invite_token(role)
156
+ click.echo(f"Invite Link: {response.registration_url}")
157
+ await questionary.press_any_key_to_continue().ask_async()
158
+
159
+ except Exception as e:
160
+ click.secho(f"An error occurred: {e}", fg="red")
161
+ await questionary.press_any_key_to_continue().ask_async()
162
+
163
+
164
+ @click.group(invoke_without_command=True)
165
+ @click.pass_context
166
+ async def users(ctx):
167
+ """Commands for managing users."""
168
+ if ctx.invoked_subcommand is None:
169
+ client = ctx.obj.get("client")
170
+ if not client:
171
+ click.secho("You are not logged in.", fg="red")
172
+ return
173
+ await interactive_user_workflow(ctx, client)
174
+
175
+
176
+ @users.command()
177
+ @pass_async_context
178
+ async def list(ctx):
179
+ """List all users."""
180
+ client = ctx.obj["client"]
181
+ users = await client.async_get_users()
182
+ for user in users:
183
+ click.echo(
184
+ f"ID: {user.id} | Username: {user.username} | Role: {user.role} | Active: {user.is_active} | Type: {user.identity_type}"
185
+ )
186
+
187
+
188
+ @users.command()
189
+ @click.argument("user_id", type=int)
190
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt")
191
+ @pass_async_context
192
+ async def delete(ctx, user_id: int, yes: bool):
193
+ """Delete a user."""
194
+ client = ctx.obj["client"]
195
+ if not yes:
196
+ confirm = await questionary.confirm(
197
+ f"Are you sure you want to delete user {user_id}?"
198
+ ).ask_async()
199
+ if not confirm:
200
+ click.echo("Aborted.")
201
+ return
202
+
203
+ response = await client.async_delete_user(user_id)
204
+ click.echo(response.model_dump_json(indent=2))
205
+
206
+
207
+ @users.command()
208
+ @click.argument("user_id", type=int)
209
+ @click.argument("role", type=click.Choice(["user", "moderator", "admin"]))
210
+ @pass_async_context
211
+ async def set_role(ctx, user_id: int, role: str):
212
+ """Set a user's role."""
213
+ client = ctx.obj["client"]
214
+ response = await client.async_update_user_role(user_id, role)
215
+ click.echo(response.model_dump_json(indent=2))
216
+
217
+
218
+ @users.command()
219
+ @click.argument("user_id", type=int)
220
+ @pass_async_context
221
+ async def enable(ctx, user_id: int):
222
+ """Enable a user account."""
223
+ client = ctx.obj["client"]
224
+ response = await client.async_enable_user(user_id)
225
+ click.echo(response.model_dump_json(indent=2))
226
+
227
+
228
+ @users.command()
229
+ @click.argument("user_id", type=int)
230
+ @pass_async_context
231
+ async def disable(ctx, user_id: int):
232
+ """Disable a user account."""
233
+ client = ctx.obj["client"]
234
+ response = await client.async_disable_user(user_id)
235
+ click.echo(response.model_dump_json(indent=2))
236
+
237
+
238
+ @users.command()
239
+ @click.argument("role", type=click.Choice(["user", "moderator", "admin"]))
240
+ @pass_async_context
241
+ async def invite(ctx, role: str):
242
+ """Generate an invite link for a new user."""
243
+ client = ctx.obj["client"]
244
+ response = await client.async_generate_invite_token(role)
245
+ click.echo(f"Invite Link: {response.registration_url}")
bsm_cli/world.py ADDED
@@ -0,0 +1,171 @@
1
+ import os
2
+
3
+ import click
4
+ import questionary
5
+ from bsm_cli.decorators import monitor_task, pass_async_context
6
+
7
+ from bsm_api_client.models import FileNamePayload
8
+
9
+
10
+ @click.group()
11
+ def world():
12
+ """Manages server worlds."""
13
+ pass
14
+
15
+
16
+ @world.command("install")
17
+ @click.option(
18
+ "-s", "--server", "server_name", required=True, help="Name of the target server."
19
+ )
20
+ @click.option(
21
+ "-f",
22
+ "--file",
23
+ "world_file_path",
24
+ type=click.Path(exists=True, dir_okay=False, resolve_path=True),
25
+ help="Path to the .mcworld file to install. Skips interactive menu.",
26
+ )
27
+ @pass_async_context
28
+ async def install_world(ctx, server_name: str, world_file_path: str):
29
+ """Installs a world from a .mcworld file, replacing the server's current world."""
30
+ client = ctx.obj.get("client")
31
+ if not client:
32
+ click.secho("You are not logged in.", fg="red")
33
+ return
34
+
35
+ try:
36
+ selected_file = world_file_path
37
+
38
+ if not selected_file:
39
+ click.secho(
40
+ f"Entering interactive world installation for server: {server_name}",
41
+ fg="yellow",
42
+ )
43
+ response = await client.async_get_content_worlds()
44
+ available_files = response.files
45
+
46
+ if not available_files:
47
+ click.secho(
48
+ "No .mcworld files found in the content/worlds directory. Nothing to install.",
49
+ fg="yellow",
50
+ )
51
+ return
52
+
53
+ file_map = {os.path.basename(f): f for f in available_files}
54
+ choices = sorted(list(file_map.keys())) + ["Cancel"]
55
+ selection = await questionary.select(
56
+ "Select a world to install:", choices=choices
57
+ ).ask_async()
58
+
59
+ if not selection or selection == "Cancel":
60
+ raise click.Abort()
61
+ selected_file = file_map[selection]
62
+
63
+ filename = os.path.basename(selected_file)
64
+ click.secho(
65
+ f"\nWARNING: Installing '{filename}' will REPLACE the current world data for server '{server_name}'.",
66
+ fg="red",
67
+ bold=True,
68
+ )
69
+ if not await questionary.confirm(
70
+ "This action cannot be undone. Are you sure?", default=False
71
+ ).ask_async():
72
+ raise click.Abort()
73
+
74
+ click.echo(f"Installing world '{filename}'...")
75
+ payload = FileNamePayload(filename=filename)
76
+ response = await client.async_install_server_world(server_name, payload)
77
+
78
+ if response.task_id:
79
+ await monitor_task(
80
+ client,
81
+ response.task_id,
82
+ f"World '{filename}' installed successfully",
83
+ "Failed to install world",
84
+ )
85
+ elif response.status == "success":
86
+ click.secho(f"World '{filename}' installed successfully.", fg="green")
87
+ else:
88
+ click.secho(f"Failed to install world: {response.message}", fg="red")
89
+
90
+ except Exception as e:
91
+ click.secho(f"An error occurred: {e}", fg="red")
92
+
93
+
94
+ @world.command("export")
95
+ @click.option(
96
+ "-s",
97
+ "--server",
98
+ "server_name",
99
+ required=True,
100
+ help="Name of the server whose world to export.",
101
+ )
102
+ @pass_async_context
103
+ async def export_world(ctx, server_name: str):
104
+ """Exports the server's current active world to a .mcworld file."""
105
+ client = ctx.obj.get("client")
106
+ if not client:
107
+ click.secho("You are not logged in.", fg="red")
108
+ return
109
+
110
+ click.echo(f"Attempting to export world for server '{server_name}'...")
111
+ try:
112
+ response = await client.async_export_server_world(server_name)
113
+ if response.task_id:
114
+ await monitor_task(
115
+ client,
116
+ response.task_id,
117
+ "World exported successfully",
118
+ "Failed to export world",
119
+ )
120
+ elif response.status == "success":
121
+ click.secho("World exported successfully.", fg="green")
122
+ else:
123
+ click.secho(f"Failed to export world: {response.message}", fg="red")
124
+ except Exception as e:
125
+ click.secho(f"An error occurred during export: {e}", fg="red")
126
+
127
+
128
+ @world.command("reset")
129
+ @click.option(
130
+ "-s",
131
+ "--server",
132
+ "server_name",
133
+ required=True,
134
+ help="Name of the server whose world to reset.",
135
+ )
136
+ @click.option("-y", "--yes", is_flag=True, help="Bypass the confirmation prompt.")
137
+ @pass_async_context
138
+ async def reset_world(ctx, server_name: str, yes: bool):
139
+ """Deletes the current active world data for a server."""
140
+ client = ctx.obj.get("client")
141
+ if not client:
142
+ click.secho("You are not logged in.", fg="red")
143
+ return
144
+
145
+ if not yes:
146
+ click.secho(
147
+ f"WARNING: This will permanently delete the current world for server '{server_name}'.",
148
+ fg="red",
149
+ bold=True,
150
+ )
151
+ click.confirm(
152
+ "This action cannot be undone. Are you sure you want to reset the world?",
153
+ abort=True,
154
+ )
155
+
156
+ click.echo(f"Resetting world for server '{server_name}'...")
157
+ try:
158
+ response = await client.async_reset_server_world(server_name)
159
+ if response.task_id:
160
+ await monitor_task(
161
+ client,
162
+ response.task_id,
163
+ "World has been reset successfully",
164
+ "Failed to reset world",
165
+ )
166
+ elif response.status == "success":
167
+ click.secho("World has been reset successfully.", fg="green")
168
+ else:
169
+ click.secho(f"Failed to reset world: {response.message}", fg="red")
170
+ except Exception as e:
171
+ click.secho(f"An error occurred during reset: {e}", fg="red")
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: bsm-cli
3
+ Version: 1.6.0b3
4
+ Summary: A CLI for managing Bedrock servers via BSM API
5
+ Author-email: DMedina559 <dmedina559-github@outlook.com>
6
+ Project-URL: Homepage, https://github.com/DMedina559/bsm-api-client
7
+ Keywords: minecraft,bedrock,server,manager,cli
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: click<8.5,>=8.2.0
18
+ Requires-Dist: questionary<2.2,>=2.1.0
19
+ Requires-Dist: bsm-api-client
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest<9.2,>=8.4.0; extra == "dev"
22
+ Requires-Dist: pytest-mock<3.16,>=3.14.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio<1.5,>=1.1.0; extra == "dev"
24
+ Requires-Dist: black<26.6,>=25.1.0; extra == "dev"
25
+ Requires-Dist: flake8<7.4,>=7.3.0; extra == "dev"
26
+ Requires-Dist: isort<8.1,>=5.13.0; extra == "dev"
27
+ Requires-Dist: mypy<2.2,>=1.10.0; extra == "dev"
28
+ Requires-Dist: bedrock-server-manager<3.11.0,>=3.10.0b3; extra == "dev"
29
+ Requires-Dist: pre-commit<4.7,>=4.6.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ <div style="text-align: center;">
33
+ <img src="https://raw.githubusercontent.com/DMedina559/bsm-frontend/main/frontend/public/image/icon/favicon.svg" alt="BSM Logo" width="150">
34
+ </div>
35
+
36
+ # bsm-cli
37
+
38
+ <p align="center">
39
+ <a href="https://github.com/DMedina559/bsm-api-client/releases">
40
+ <img alt="Stable" src="https://img.shields.io/github/v/release/DMedina559/bsm-api-client?label=Stable&color=blue">
41
+ </a>
42
+ <a href="https://github.com/DMedina559/bsm-api-client/releases">
43
+ <img alt="Pre-Release" src="https://img.shields.io/github/v/release/DMedina559/bsm-api-client?include_prereleases&label=Pre-Release&color=red">
44
+ </a>
45
+ <a href="https://github.com/DMedina559/bsm-api-client/actions">
46
+ <img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/DMedina559/bsm-api-client/build-test.yml?label=Tests&event=push">
47
+ </a>
48
+ </p>
49
+
50
+ ## Introduction
51
+
52
+ `bsm-cli` is a command-line interface tool for managing Minecraft Bedrock Dedicated Servers via the Bedrock Server Manager API.
53
+
54
+ ## Features
55
+
56
+ * Full CLI interface using `click` and `questionary`.
57
+ * Interactive menus for server management.
58
+ * Realtime server state updates via WebSocket.
59
+ * Seamlessly manages configuration for server and backups.
60
+
61
+ ## Installation
62
+
63
+ Install the library using pip:
64
+
65
+ ```bash
66
+ pip install bsm-cli
67
+ ```
68
+
69
+ ## Quick Start
70
+
71
+ You can invoke the CLI using:
72
+
73
+ ```bash
74
+ bsm-cli
75
+ ```
76
+
77
+ Which will trigger the interactive menu allowing you to manage and configure your Bedrock Dedicated Servers.
@@ -0,0 +1,26 @@
1
+ bsm_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ bsm_cli/__main__.py,sha256=Jb5mbgqzvXtgoy2qW8yvNPqMnq7jbBJfPJurnOUxszc,2221
3
+ bsm_cli/account.py,sha256=xnaFNhuskKt3biYnwO6pePkGb_iYGKOE5PVsMD6SHu4,2156
4
+ bsm_cli/addon.py,sha256=xfq_zEfS5OreoRnhSLSmP5y_sx6zkCOqzLkdd-8KdKs,14072
5
+ bsm_cli/allowlist.py,sha256=fqyaa8YwsCV9KuI3Lh79vmCoUr5KqgFFbPd2skxx72k,7814
6
+ bsm_cli/auth.py,sha256=AJK7daic0TbM15GFn77bn9WdFuH14fZtR3nfLJRvLCM,3012
7
+ bsm_cli/backup.py,sha256=-GpRTX2AnEGYBVXxlciKKOMmjn6Y32Ei5y7g6JlFOrU,8792
8
+ bsm_cli/bans.py,sha256=9ohs2ofAvXT022hWyE5Ibbj6rrzELua88FGFFlp_KY0,6024
9
+ bsm_cli/config.py,sha256=iRrilB7b9g6P79O4pznBz--M3d8555j1qAQXsXn-jVI,2528
10
+ bsm_cli/content.py,sha256=F631Dpryi_UiFnkhCk-aVbrNlDyyaS7LjOCFunMgbQg,513
11
+ bsm_cli/decorators.py,sha256=9-bsWWCOUQUDrX7oRRD4gvzqKRPVOUXcSFgGrvxtM_U,5031
12
+ bsm_cli/main_menus.py,sha256=XuuTU49_Ypo1lT1AuUbwu643LDCyrIdSU3hCox5yjn4,10546
13
+ bsm_cli/permissions.py,sha256=5DQ0-TFERdwjDWBFPC8ZyEWLnsAPgjKO4vaxqSrb2zU,6356
14
+ bsm_cli/player.py,sha256=vQygt9j8SAgAZKydz5lWJkEiPWkLJ3u2hYGvBjQxdc4,1922
15
+ bsm_cli/plugins.py,sha256=gMEqRgX8pe_4fg6UtVLGtybLOmZu-CaJj_yC5xi-vTY,10400
16
+ bsm_cli/properties.py,sha256=eoGQE_-g3Y9GbM_nbb3hClH1SPd8poV6pWXERT2Kwe0,7021
17
+ bsm_cli/server.py,sha256=GfQ1fl-Z6u6409_HwUaS3jvEqbULLwqjqADxbDJ63Wc,17309
18
+ bsm_cli/system.py,sha256=y1XAbiWv0TaGpbE0LMfkMVfyY18L8RoMWVS3eijyLNM,4886
19
+ bsm_cli/users.py,sha256=lg2dkaUyay9F1rJnYY2XgLeW0MX9PI74vSTK3n4_sm0,9550
20
+ bsm_cli/world.py,sha256=AszZrZmWhJD_xioasZldxfLWfCxjqwLhn38KQhz0zgI,5653
21
+ bsm_cli-1.6.0b3.dist-info/licenses/LICENSE,sha256=dsRN6HXDmbpw1K4fTVXgOrzgsWZOjJ03O09ZkjQUR_A,1062
22
+ bsm_cli-1.6.0b3.dist-info/METADATA,sha256=BIdWVEXn7n94OsYZrKW8kEPil-mpucZNAGgxeH58i5g,2782
23
+ bsm_cli-1.6.0b3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
24
+ bsm_cli-1.6.0b3.dist-info/entry_points.txt,sha256=VvvRZ2PKz2fxBr36ZXiG1Fu84KVMgAr7Vtsq8X3IQr4,49
25
+ bsm_cli-1.6.0b3.dist-info/top_level.txt,sha256=b9po-lHYQ4duM9sNLw8dxxhhj3ow8cTBHAy61cIs4Ys,8
26
+ bsm_cli-1.6.0b3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bsm-cli = bsm_cli.__main__:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Danny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ bsm_cli