cal-docs-client 1.0.0b1__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.
- cal_docs_client/__init__.py +20 -0
- cal_docs_client/__main__.py +21 -0
- cal_docs_client/_version.py +4 -0
- cal_docs_client/argbuilder.py +1424 -0
- cal_docs_client/cli.py +518 -0
- cal_docs_client/client.py +265 -0
- cal_docs_client/common/__init__.py +32 -0
- cal_docs_client/common/colour.py +107 -0
- cal_docs_client/version.py +32 -0
- cal_docs_client-1.0.0b1.dist-info/METADATA +120 -0
- cal_docs_client-1.0.0b1.dist-info/RECORD +14 -0
- cal_docs_client-1.0.0b1.dist-info/WHEEL +4 -0
- cal_docs_client-1.0.0b1.dist-info/entry_points.txt +2 -0
- cal_docs_client-1.0.0b1.dist-info/licenses/LICENSE +21 -0
cal_docs_client/cli.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# cli.py
|
|
3
|
+
# ──────
|
|
4
|
+
#
|
|
5
|
+
# Command-line interface for cal-docs-client.
|
|
6
|
+
#
|
|
7
|
+
# (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
|
|
8
|
+
#
|
|
9
|
+
# Authors
|
|
10
|
+
# ───────
|
|
11
|
+
# bena (via Claude)
|
|
12
|
+
#
|
|
13
|
+
# Version History
|
|
14
|
+
# ───────────────
|
|
15
|
+
# Feb 2026 - Created
|
|
16
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
# Imports
|
|
20
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import traceback
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
from typing import cast
|
|
30
|
+
from .argbuilder import ArgsParser
|
|
31
|
+
from .argbuilder import Namespace
|
|
32
|
+
from .client import ClientError
|
|
33
|
+
from .client import DocsClient
|
|
34
|
+
from .common import bold
|
|
35
|
+
from .common import cyan
|
|
36
|
+
from .common import green
|
|
37
|
+
from .common import red
|
|
38
|
+
from .common import set_colours_enabled
|
|
39
|
+
from .common import yellow
|
|
40
|
+
from .version import VERSION_STR
|
|
41
|
+
|
|
42
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
# Constants
|
|
44
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
CONFIG_DIR = Path.home() / ".config" / "cal-docs-client"
|
|
47
|
+
DEFAULT_CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
48
|
+
|
|
49
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
# Config Loading
|
|
51
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---
|
|
55
|
+
def load_config(config_path: Path | None) -> dict[str, Any]:
|
|
56
|
+
"""Load configuration from a JSON file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config_path: Path to config file, or None to use default.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Configuration dict, or empty dict if not found.
|
|
63
|
+
|
|
64
|
+
Config file format:
|
|
65
|
+
{
|
|
66
|
+
"server": "https://docs.example.com",
|
|
67
|
+
"token": "your-api-token",
|
|
68
|
+
"no_colour": false
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
# Determine which config file to use
|
|
72
|
+
if config_path:
|
|
73
|
+
path = config_path
|
|
74
|
+
else:
|
|
75
|
+
path = DEFAULT_CONFIG_PATH
|
|
76
|
+
|
|
77
|
+
if not path.exists():
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with path.open() as f:
|
|
82
|
+
data = json.load(f)
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
print(
|
|
85
|
+
yellow(
|
|
86
|
+
f"Warning: Config file {path} is not a JSON object, ignoring"
|
|
87
|
+
),
|
|
88
|
+
file=sys.stderr,
|
|
89
|
+
)
|
|
90
|
+
return {}
|
|
91
|
+
return cast("dict[str, Any]", data)
|
|
92
|
+
except json.JSONDecodeError as e:
|
|
93
|
+
print(
|
|
94
|
+
yellow(f"Warning: Invalid JSON in config file {path}: {e}"),
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
return {}
|
|
98
|
+
except OSError as e:
|
|
99
|
+
print(
|
|
100
|
+
yellow(f"Warning: Could not read config file {path}: {e}"),
|
|
101
|
+
file=sys.stderr,
|
|
102
|
+
)
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
# Argument Parser
|
|
108
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---
|
|
112
|
+
def create_parser() -> ArgsParser:
|
|
113
|
+
"""Create the main argument parser with subcommands."""
|
|
114
|
+
parser = ArgsParser(
|
|
115
|
+
prog="cal-docs-client",
|
|
116
|
+
description="CLI client for cal-docs-server documentation API",
|
|
117
|
+
version=f"cal-docs-client {VERSION_STR}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# =========== Common Options ===========
|
|
121
|
+
|
|
122
|
+
common = parser.create_common_collection()
|
|
123
|
+
common_group = common.add_group("Common Options")
|
|
124
|
+
common_group.add_argument(
|
|
125
|
+
"-s",
|
|
126
|
+
"--server",
|
|
127
|
+
metavar="URL",
|
|
128
|
+
help="Server URL (or set CAL_DOCS_SERVER env var, or config file)",
|
|
129
|
+
)
|
|
130
|
+
common_group.add_argument(
|
|
131
|
+
"-c",
|
|
132
|
+
"--config",
|
|
133
|
+
metavar="FILE",
|
|
134
|
+
type=Path,
|
|
135
|
+
help=f"Config file path (default: {DEFAULT_CONFIG_PATH})",
|
|
136
|
+
)
|
|
137
|
+
common_group.add_argument(
|
|
138
|
+
"--no-colour",
|
|
139
|
+
"--no-color",
|
|
140
|
+
action="store_true",
|
|
141
|
+
dest="no_colour",
|
|
142
|
+
help="Disable coloured output",
|
|
143
|
+
)
|
|
144
|
+
common_group.add_argument(
|
|
145
|
+
"-v",
|
|
146
|
+
"--verbose",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="Verbose output",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# =========== Version Command ===========
|
|
152
|
+
|
|
153
|
+
parser.add_command(
|
|
154
|
+
"version",
|
|
155
|
+
help="Show server and client version",
|
|
156
|
+
description="Show version information for both the server and this client.",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# =========== Projects Command ===========
|
|
160
|
+
|
|
161
|
+
projects_cmd = parser.add_command(
|
|
162
|
+
"projects",
|
|
163
|
+
help="List documentation projects",
|
|
164
|
+
description="List all documentation projects on the server.",
|
|
165
|
+
)
|
|
166
|
+
projects_cmd.add_argument(
|
|
167
|
+
"-q",
|
|
168
|
+
"--search",
|
|
169
|
+
metavar="TERM",
|
|
170
|
+
help="Filter projects by name",
|
|
171
|
+
)
|
|
172
|
+
projects_cmd.add_argument(
|
|
173
|
+
"-j",
|
|
174
|
+
"--json",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="Output as JSON",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# =========== Download Command ===========
|
|
180
|
+
|
|
181
|
+
download_cmd = parser.add_command(
|
|
182
|
+
"download",
|
|
183
|
+
help="Download documentation",
|
|
184
|
+
description="Download a documentation package as a zip file.",
|
|
185
|
+
)
|
|
186
|
+
download_cmd.add_argument(
|
|
187
|
+
"project",
|
|
188
|
+
help="Project name",
|
|
189
|
+
)
|
|
190
|
+
download_cmd.add_argument(
|
|
191
|
+
"doc_version",
|
|
192
|
+
nargs="?",
|
|
193
|
+
default="latest",
|
|
194
|
+
metavar="VERSION",
|
|
195
|
+
help="Version to download (default: latest)",
|
|
196
|
+
)
|
|
197
|
+
download_cmd.add_argument(
|
|
198
|
+
"-o",
|
|
199
|
+
"--output",
|
|
200
|
+
metavar="FILE",
|
|
201
|
+
help="Output filename (default: {project}-{version}-docs.zip)",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# =========== Upload Command ===========
|
|
205
|
+
|
|
206
|
+
upload_cmd = parser.add_command(
|
|
207
|
+
"upload",
|
|
208
|
+
help="Upload documentation",
|
|
209
|
+
description=(
|
|
210
|
+
"Upload a documentation package to the server.\nRequires authentication via"
|
|
211
|
+
" --token, CAL_DOCS_TOKEN env var, or config file.\nFilename must match:"
|
|
212
|
+
" {project}-{version}[-docs].zip"
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
upload_cmd.add_argument(
|
|
216
|
+
"file",
|
|
217
|
+
help="Zip file to upload",
|
|
218
|
+
)
|
|
219
|
+
upload_cmd.add_argument(
|
|
220
|
+
"-t",
|
|
221
|
+
"--token",
|
|
222
|
+
metavar="TOKEN",
|
|
223
|
+
help="Auth token (or set CAL_DOCS_TOKEN env var, or config file)",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# =========== Help Command ===========
|
|
227
|
+
|
|
228
|
+
parser.add_command(
|
|
229
|
+
"help",
|
|
230
|
+
help="Show server API help",
|
|
231
|
+
description="Fetch and display the server's API help documentation.",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# =========== Spec Command ===========
|
|
235
|
+
|
|
236
|
+
spec_cmd = parser.add_command(
|
|
237
|
+
"spec",
|
|
238
|
+
help="Show OpenAPI specification",
|
|
239
|
+
description="Fetch and display the server's OpenAPI specification.",
|
|
240
|
+
)
|
|
241
|
+
spec_cmd.add_argument(
|
|
242
|
+
"-o",
|
|
243
|
+
"--output",
|
|
244
|
+
metavar="FILE",
|
|
245
|
+
help="Save specification to file",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return parser
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
252
|
+
# Command Handlers
|
|
253
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---
|
|
257
|
+
def cmd_version(client: DocsClient, _args: Namespace) -> int:
|
|
258
|
+
"""Handle the 'version' command."""
|
|
259
|
+
client.verify_server()
|
|
260
|
+
|
|
261
|
+
print(
|
|
262
|
+
f"{bold('Server:')} cal-docs-server {client.server_version} (API"
|
|
263
|
+
f" v{client.api_version})"
|
|
264
|
+
)
|
|
265
|
+
print(f"{bold('Client:')} cal-docs-client {VERSION_STR}")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---
|
|
270
|
+
def cmd_projects(client: DocsClient, args: Namespace) -> int:
|
|
271
|
+
"""Handle the 'projects' command."""
|
|
272
|
+
client.verify_server()
|
|
273
|
+
|
|
274
|
+
data = client.get_projects(args.search)
|
|
275
|
+
|
|
276
|
+
if args.json:
|
|
277
|
+
print(json.dumps(data, indent=2))
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
projects = data.get("projects", [])
|
|
281
|
+
count = data.get("count", len(projects))
|
|
282
|
+
|
|
283
|
+
if args.search:
|
|
284
|
+
print(
|
|
285
|
+
f"{bold('Projects')} matching '{args.search}' on {cyan(client.server_url)}"
|
|
286
|
+
f" ({count} found):\n"
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
print(f"{bold('Projects')} on {cyan(client.server_url)} ({count} found):\n")
|
|
290
|
+
|
|
291
|
+
if not projects:
|
|
292
|
+
print(yellow(" No projects found."))
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
for proj in projects:
|
|
296
|
+
name = proj.get("name", proj.get("directory_name", "unknown"))
|
|
297
|
+
desc = proj.get("description", "")
|
|
298
|
+
versions = proj.get("versions", [])
|
|
299
|
+
latest = proj.get("latest_version_dir")
|
|
300
|
+
|
|
301
|
+
print(f" {bold(name)}")
|
|
302
|
+
if desc:
|
|
303
|
+
print(f" {desc}")
|
|
304
|
+
|
|
305
|
+
if versions:
|
|
306
|
+
version_strs = [v.get("version", "?") for v in versions[:5]]
|
|
307
|
+
if len(versions) > 5:
|
|
308
|
+
version_strs.append(f"... ({len(versions)} total)")
|
|
309
|
+
print(f" Versions: {', '.join(version_strs)}")
|
|
310
|
+
|
|
311
|
+
if latest:
|
|
312
|
+
print(
|
|
313
|
+
" Latest:"
|
|
314
|
+
f" {green(latest.rsplit('-', 1)[-1] if '-' in latest else latest)}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
print()
|
|
318
|
+
|
|
319
|
+
return 0
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---
|
|
323
|
+
def cmd_download(client: DocsClient, args: Namespace) -> int:
|
|
324
|
+
"""Handle the 'download' command."""
|
|
325
|
+
client.verify_server()
|
|
326
|
+
|
|
327
|
+
project = args.project
|
|
328
|
+
version = args.doc_version
|
|
329
|
+
|
|
330
|
+
# Determine output filename
|
|
331
|
+
if args.output:
|
|
332
|
+
output_path = Path(args.output)
|
|
333
|
+
else:
|
|
334
|
+
output_path = Path(f"{project}-{version}-docs.zip")
|
|
335
|
+
|
|
336
|
+
print(f"Downloading {cyan(project)} version {cyan(version)}...")
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
data = client.download(project, version)
|
|
340
|
+
except ClientError as e:
|
|
341
|
+
print(red(f"Error: {e}"), file=sys.stderr)
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
output_path.write_bytes(data)
|
|
345
|
+
print(f"Saved to {green(str(output_path))} ({len(data):,} bytes)")
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---
|
|
350
|
+
def cmd_upload(client: DocsClient, args: Namespace) -> int:
|
|
351
|
+
"""Handle the 'upload' command."""
|
|
352
|
+
client.verify_server()
|
|
353
|
+
|
|
354
|
+
file_path = Path(args.file)
|
|
355
|
+
|
|
356
|
+
if not file_path.exists():
|
|
357
|
+
print(red(f"Error: File not found: {file_path}"), file=sys.stderr)
|
|
358
|
+
return 1
|
|
359
|
+
|
|
360
|
+
if not file_path.suffix.lower() == ".zip":
|
|
361
|
+
print(red("Error: File must be a .zip file"), file=sys.stderr)
|
|
362
|
+
return 1
|
|
363
|
+
|
|
364
|
+
if not client.token:
|
|
365
|
+
print(
|
|
366
|
+
red(
|
|
367
|
+
"Error: Authentication required. Set --token, CAL_DOCS_TOKEN env var,"
|
|
368
|
+
" or add token to config file."
|
|
369
|
+
),
|
|
370
|
+
file=sys.stderr,
|
|
371
|
+
)
|
|
372
|
+
return 1
|
|
373
|
+
|
|
374
|
+
filename = file_path.name
|
|
375
|
+
data = file_path.read_bytes()
|
|
376
|
+
|
|
377
|
+
print(f"Uploading {cyan(filename)} ({len(data):,} bytes)...")
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
result = client.upload(filename, data)
|
|
381
|
+
except ClientError as e:
|
|
382
|
+
print(red(f"Error: {e}"), file=sys.stderr)
|
|
383
|
+
return 1
|
|
384
|
+
|
|
385
|
+
if result.get("success"):
|
|
386
|
+
print(green("Upload successful!"))
|
|
387
|
+
print(f" Project: {result.get('project', 'unknown')}")
|
|
388
|
+
print(f" Version: {result.get('version', 'unknown')}")
|
|
389
|
+
print(f" URL: {result.get('url', 'N/A')}")
|
|
390
|
+
print(f" Files: {result.get('files_extracted', 'N/A')}")
|
|
391
|
+
else:
|
|
392
|
+
print(
|
|
393
|
+
red(f"Upload failed: {result.get('message', 'Unknown error')}"),
|
|
394
|
+
file=sys.stderr,
|
|
395
|
+
)
|
|
396
|
+
return 1
|
|
397
|
+
|
|
398
|
+
return 0
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ---
|
|
402
|
+
def cmd_help(client: DocsClient, _args: Namespace) -> int:
|
|
403
|
+
"""Handle the 'help' command (server API help)."""
|
|
404
|
+
client.verify_server()
|
|
405
|
+
|
|
406
|
+
help_text = client.get_help()
|
|
407
|
+
print(help_text)
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---
|
|
412
|
+
def cmd_spec(client: DocsClient, args: Namespace) -> int:
|
|
413
|
+
"""Handle the 'spec' command."""
|
|
414
|
+
client.verify_server()
|
|
415
|
+
|
|
416
|
+
spec = client.get_spec()
|
|
417
|
+
output = json.dumps(spec, indent=2)
|
|
418
|
+
|
|
419
|
+
if args.output:
|
|
420
|
+
Path(args.output).write_text(output)
|
|
421
|
+
print(f"Specification saved to {green(args.output)}")
|
|
422
|
+
else:
|
|
423
|
+
print(output)
|
|
424
|
+
|
|
425
|
+
return 0
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
429
|
+
# Main Entry Point
|
|
430
|
+
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
CommandHandler = Callable[[DocsClient, Namespace], int]
|
|
433
|
+
|
|
434
|
+
COMMANDS: dict[str, CommandHandler] = {
|
|
435
|
+
"version": cmd_version,
|
|
436
|
+
"projects": cmd_projects,
|
|
437
|
+
"download": cmd_download,
|
|
438
|
+
"upload": cmd_upload,
|
|
439
|
+
"help": cmd_help,
|
|
440
|
+
"spec": cmd_spec,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ---
|
|
445
|
+
def main(argv: list[str] | None = None) -> int:
|
|
446
|
+
"""Main entry point."""
|
|
447
|
+
if argv is None:
|
|
448
|
+
argv = sys.argv[1:]
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
return _main_inner(argv)
|
|
452
|
+
except KeyboardInterrupt:
|
|
453
|
+
print()
|
|
454
|
+
print("---- Manually Terminated ----")
|
|
455
|
+
print()
|
|
456
|
+
return 1
|
|
457
|
+
except SystemExit as e:
|
|
458
|
+
return e.code if isinstance(e.code, int) else 1
|
|
459
|
+
except BaseException as e:
|
|
460
|
+
print("-" * 77, file=sys.stderr)
|
|
461
|
+
print("UNHANDLED EXCEPTION OCCURRED!!", file=sys.stderr)
|
|
462
|
+
print(file=sys.stderr)
|
|
463
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
464
|
+
print(f"EXCEPTION: {type(e).__name__}: {e}", file=sys.stderr)
|
|
465
|
+
print("-" * 77, file=sys.stderr)
|
|
466
|
+
return 1
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ---
|
|
470
|
+
def _main_inner(argv: list[str]) -> int:
|
|
471
|
+
"""Inner main function that may raise exceptions."""
|
|
472
|
+
parser = create_parser()
|
|
473
|
+
args = parser.parse(argv)
|
|
474
|
+
|
|
475
|
+
# Load config file (CLI arg > default path)
|
|
476
|
+
config = load_config(args.config)
|
|
477
|
+
|
|
478
|
+
# Handle colour settings: CLI > config > default (auto)
|
|
479
|
+
if args.no_colour:
|
|
480
|
+
set_colours_enabled(False)
|
|
481
|
+
elif config.get("no_colour"):
|
|
482
|
+
set_colours_enabled(False)
|
|
483
|
+
|
|
484
|
+
# If no command, show help
|
|
485
|
+
if not args.command:
|
|
486
|
+
return 0 # ArgsParser already shows help
|
|
487
|
+
|
|
488
|
+
# Get server URL: CLI > env > config
|
|
489
|
+
server_url = (
|
|
490
|
+
args.server or os.environ.get("CAL_DOCS_SERVER") or config.get("server")
|
|
491
|
+
)
|
|
492
|
+
if not server_url:
|
|
493
|
+
print(
|
|
494
|
+
red(
|
|
495
|
+
"Error: Server URL required. Use --server, set CAL_DOCS_SERVER env var,"
|
|
496
|
+
" or add to config file."
|
|
497
|
+
),
|
|
498
|
+
file=sys.stderr,
|
|
499
|
+
)
|
|
500
|
+
return 1
|
|
501
|
+
|
|
502
|
+
# Get token: CLI > env > config
|
|
503
|
+
token = args.token or os.environ.get("CAL_DOCS_TOKEN") or config.get("token")
|
|
504
|
+
|
|
505
|
+
# Create client
|
|
506
|
+
client = DocsClient(server_url, token)
|
|
507
|
+
|
|
508
|
+
# Get command handler
|
|
509
|
+
handler = COMMANDS.get(args.command)
|
|
510
|
+
if not handler:
|
|
511
|
+
print(red(f"Error: Unknown command: {args.command}"), file=sys.stderr)
|
|
512
|
+
return 1
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
return handler(client, args)
|
|
516
|
+
except ClientError as e:
|
|
517
|
+
print(red(f"Error: {e}"), file=sys.stderr)
|
|
518
|
+
return 1
|