elemctl 0.1.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.
elemctl/cli.py ADDED
@@ -0,0 +1,651 @@
1
+ """Консольный интерфейс elemctl.
2
+
3
+ Соглашения вывода: результат – JSON в stdout (ensure_ascii=False, отступ 2);
4
+ прогресс длительных операций – строки в stderr; ошибки – JSON с полем error
5
+ в stderr и код возврата 1.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from . import __version__
16
+ from .build import build_assembly
17
+ from .client import ElementClient, extract_assembly_id
18
+ from .config import Config
19
+ from .deploy import deploy_from_platform_branch, deploy_from_sources
20
+ from .errors import ApiError, ConfigError, ElemctlError
21
+
22
+
23
+ def make_client(config):
24
+ """Фабрика клиента; выделена, чтобы тесты могли её подменить."""
25
+ return ElementClient(config)
26
+
27
+
28
+ # -- вывод ---------------------------------------------------------------------
29
+
30
+
31
+ def _emit(data):
32
+ print(json.dumps(data, ensure_ascii=False, indent=2))
33
+
34
+
35
+ def _progress(message):
36
+ print(message, file=sys.stderr)
37
+
38
+
39
+ def _fail(payload):
40
+ print(json.dumps(payload, ensure_ascii=False, indent=2), file=sys.stderr)
41
+ return 1
42
+
43
+
44
+ def _reconfigure_streams():
45
+ """Перевести вывод консоли в UTF-8 – иначе на Windows ломается кириллица."""
46
+ for stream in (sys.stdout, sys.stderr):
47
+ reconfigure = getattr(stream, "reconfigure", None)
48
+ if reconfigure is not None:
49
+ try:
50
+ reconfigure(encoding="utf-8")
51
+ except (OSError, ValueError):
52
+ pass
53
+
54
+
55
+ # -- конфигурация и разрешение идентификаторов ------------------------------------
56
+
57
+
58
+ def _config(args):
59
+ return Config.from_env(
60
+ env_file=args.env_file,
61
+ base_url=args.base_url,
62
+ client_id=args.client_id,
63
+ client_secret=args.client_secret,
64
+ timeout=args.timeout,
65
+ )
66
+
67
+
68
+ def _require(explicit, fallback, what):
69
+ """Взять явное значение либо значение из конфигурации; иначе ошибка."""
70
+ value = explicit or fallback
71
+ if not value:
72
+ raise ConfigError(f"не задан {what}")
73
+ return value
74
+
75
+
76
+ # -- обработчики команд -----------------------------------------------------------
77
+
78
+
79
+ def cmd_token(args):
80
+ client = make_client(_config(args))
81
+ _emit({"token": client.token()})
82
+ return 0
83
+
84
+
85
+ def cmd_apps_list(args):
86
+ client = make_client(_config(args))
87
+ _emit(client.list_apps(name=args.name or ""))
88
+ return 0
89
+
90
+
91
+ def cmd_apps_get(args):
92
+ config = _config(args)
93
+ client = make_client(config)
94
+ app_id = _require(args.app_id, config.app_id, "app-id (аргумент APP_ID или ELEMENT_APP_ID)")
95
+ _emit(client.get_app(app_id))
96
+ return 0
97
+
98
+
99
+ def cmd_apps_find(args):
100
+ client = make_client(_config(args))
101
+ app = client.find_app(args.name)
102
+ if app is None:
103
+ _emit({"id": None, "found": False})
104
+ return 1
105
+ _emit({"id": app.get("id"), "found": True})
106
+ return 0
107
+
108
+
109
+ def cmd_apps_create(args):
110
+ config = _config(args)
111
+ client = make_client(config)
112
+ project_id = args.project_id or config.project_id
113
+ version_id = args.version_id
114
+
115
+ if args.latest_build and not version_id:
116
+ if not project_id:
117
+ raise ConfigError("для --latest-build нужен --project-id (или ELEMENT_PROJECT_ID)")
118
+ latest = client.latest_assembly(project_id)
119
+ if latest is None:
120
+ raise ElemctlError(
121
+ f"у проекта {project_id} нет сборок – загрузите сборку (builds upload) "
122
+ "или укажите --version-id"
123
+ )
124
+ version_id = extract_assembly_id(latest)
125
+
126
+ kwargs = {
127
+ "development_mode": not args.no_dev_mode,
128
+ "space_id": args.space_id or config.space_id or None,
129
+ "technology_version": args.tech_version or None,
130
+ }
131
+ if version_id:
132
+ card = client.create_app(args.name, project_version_id=version_id, **kwargs)
133
+ elif project_id:
134
+ _progress(
135
+ "внимание: источник – проект целиком; на части конфигураций платформы "
136
+ "это даёт пустой каркас (надёжнее --latest-build)"
137
+ )
138
+ card = client.create_app(args.name, image_id=project_id, **kwargs)
139
+ else:
140
+ raise ConfigError("нужен источник приложения: --version-id либо --project-id [--latest-build]")
141
+
142
+ if args.wait:
143
+ app_id = (card or {}).get("id")
144
+ if app_id:
145
+ card = client.wait_app_ready(app_id, log=_progress)
146
+ _emit(card)
147
+ return 0
148
+
149
+
150
+ def cmd_apps_delete(args):
151
+ client = make_client(_config(args))
152
+ response = client.delete_app(args.app_id)
153
+ _emit(response if response is not None else {"deleted": True, "app-id": args.app_id})
154
+ return 0
155
+
156
+
157
+ def cmd_apps_start(args):
158
+ config = _config(args)
159
+ client = make_client(config)
160
+ app_id = _require(args.app_id, config.app_id, "app-id (аргумент APP_ID или ELEMENT_APP_ID)")
161
+ response = client.start_app(app_id)
162
+ _emit(response if response is not None else {"ok": True, "app-id": app_id})
163
+ return 0
164
+
165
+
166
+ def cmd_apps_stop(args):
167
+ config = _config(args)
168
+ client = make_client(config)
169
+ app_id = _require(args.app_id, config.app_id, "app-id (аргумент APP_ID или ELEMENT_APP_ID)")
170
+ response = client.stop_app(app_id)
171
+ _emit(response if response is not None else {"ok": True, "app-id": app_id})
172
+ return 0
173
+
174
+
175
+ def cmd_spaces_list(args):
176
+ client = make_client(_config(args))
177
+ _emit(client.list_spaces())
178
+ return 0
179
+
180
+
181
+ def cmd_projects_list(args):
182
+ client = make_client(_config(args))
183
+ _emit(client.list_projects())
184
+ return 0
185
+
186
+
187
+ def cmd_projects_get(args):
188
+ config = _config(args)
189
+ client = make_client(config)
190
+ project_id = _require(
191
+ args.project_id, config.project_id, "project-id (аргумент PROJECT_ID или ELEMENT_PROJECT_ID)"
192
+ )
193
+ _emit(client.get_project(project_id))
194
+ return 0
195
+
196
+
197
+ def cmd_projects_delete(args):
198
+ client = make_client(_config(args))
199
+ response = client.delete_project(args.project_id)
200
+ _emit(response if response is not None else {"deleted": True, "project-id": args.project_id})
201
+ return 0
202
+
203
+
204
+ def cmd_builds_list(args):
205
+ config = _config(args)
206
+ client = make_client(config)
207
+ project_id = _require(
208
+ args.project_id, config.project_id, "--project-id (или ELEMENT_PROJECT_ID)"
209
+ )
210
+ _emit(client.list_assemblies(project_id))
211
+ return 0
212
+
213
+
214
+ def cmd_builds_get(args):
215
+ config = _config(args)
216
+ client = make_client(config)
217
+ project_id = _require(
218
+ args.project_id, config.project_id, "--project-id (или ELEMENT_PROJECT_ID)"
219
+ )
220
+ _emit(client.get_assembly(project_id, args.version))
221
+ return 0
222
+
223
+
224
+ def cmd_builds_upload(args):
225
+ config = _config(args)
226
+ client = make_client(config)
227
+ file_path = Path(args.file)
228
+ if not file_path.is_file():
229
+ raise ElemctlError(f"файл сборки не найден: {file_path}")
230
+ response = client.upload_assembly(
231
+ file_path.read_bytes(),
232
+ project_id=args.project_id or config.project_id or None,
233
+ space_id=args.space_id or config.space_id or None,
234
+ branch_name=args.branch or None,
235
+ commit_id=args.commit or None,
236
+ commit_message=args.commit_message or None,
237
+ )
238
+ _emit({"assembly-id": extract_assembly_id(response), "response": response})
239
+ return 0
240
+
241
+
242
+ def cmd_builds_delete(args):
243
+ config = _config(args)
244
+ client = make_client(config)
245
+ project_id = _require(
246
+ args.project_id, config.project_id, "--project-id (или ELEMENT_PROJECT_ID)"
247
+ )
248
+ response = client.delete_assembly(project_id, args.version)
249
+ _emit(response if response is not None else {"deleted": True, "version": args.version})
250
+ return 0
251
+
252
+
253
+ def cmd_build(args):
254
+ result = build_assembly(
255
+ args.project_dir,
256
+ output_dir=args.output,
257
+ version=args.build_version or "",
258
+ last_build_version=args.last_build or "",
259
+ branch=args.branch,
260
+ commit=args.commit,
261
+ kind=args.kind or "",
262
+ )
263
+ _emit({"file": str(result.file)})
264
+ return 0
265
+
266
+
267
+ def cmd_deploy(args):
268
+ if args.dry_run:
269
+ result = build_assembly(
270
+ args.project_dir,
271
+ output_dir=args.output,
272
+ version=args.build_version or "",
273
+ branch=args.branch,
274
+ commit=args.commit,
275
+ )
276
+ _emit({"file": str(result.file)})
277
+ return 0
278
+
279
+ config = _config(args)
280
+ client = make_client(config)
281
+ app_id = _require(args.app_id, config.app_id, "--app-id (или ELEMENT_APP_ID)")
282
+
283
+ if args.from_branch:
284
+ branch_id = _require(args.branch_id, config.branch_id, "--branch-id (или ELEMENT_BRANCH_ID)")
285
+ report = deploy_from_platform_branch(client, app_id, branch_id, log=_progress)
286
+ else:
287
+ project_id = _require(
288
+ args.project_id, config.project_id, "--project-id (или ELEMENT_PROJECT_ID)"
289
+ )
290
+ report = deploy_from_sources(
291
+ client,
292
+ app_id,
293
+ project_id,
294
+ project_dir=args.project_dir,
295
+ output_dir=args.output,
296
+ version=args.build_version or "",
297
+ branch=args.branch,
298
+ commit=args.commit,
299
+ commit_message=args.commit_message or "",
300
+ log=_progress,
301
+ )
302
+ _emit(report.to_dict())
303
+ return 0 if report.ok else 1
304
+
305
+
306
+ def cmd_branches_list(args):
307
+ client = make_client(_config(args))
308
+ _emit(client.list_branches(project_id=args.project_id or "", name=args.name or ""))
309
+ return 0
310
+
311
+
312
+ def cmd_branches_get(args):
313
+ client = make_client(_config(args))
314
+ _emit(client.get_branch(args.branch_id))
315
+ return 0
316
+
317
+
318
+ def cmd_branches_create(args):
319
+ config = _config(args)
320
+ client = make_client(config)
321
+ project_id = _require(
322
+ args.project_id, config.project_id, "--project-id (или ELEMENT_PROJECT_ID)"
323
+ )
324
+ _emit(client.create_branch(args.name, project_id, app_id=args.app_id or None))
325
+ return 0
326
+
327
+
328
+ def cmd_branches_update(args):
329
+ client = make_client(_config(args))
330
+ _emit(client.update_branch(args.branch_id, app_id=args.app_id or None))
331
+ return 0
332
+
333
+
334
+ def cmd_branches_delete(args):
335
+ client = make_client(_config(args))
336
+ response = client.delete_branch(args.branch_id)
337
+ _emit(response if response is not None else {"deleted": True, "branch-id": args.branch_id})
338
+ return 0
339
+
340
+
341
+ def cmd_branches_merge(args):
342
+ client = make_client(_config(args))
343
+ response = client.merge_branch(args.branch_id)
344
+ _emit(response if response is not None else {"merged": True, "branch-id": args.branch_id})
345
+ return 0
346
+
347
+
348
+ def cmd_branches_sync(args):
349
+ config = _config(args)
350
+ client = make_client(config)
351
+ branch_id = _require(args.branch_id, config.branch_id, "branch-id (аргумент ID или ELEMENT_BRANCH_ID)")
352
+ app_id = _require(args.app_id, config.app_id, "--app-id (или ELEMENT_APP_ID)")
353
+ response = client.sync_branch(app_id, branch_id)
354
+ _emit(response if response is not None else {"ok": True, "app-id": app_id, "branch-id": branch_id})
355
+ return 0
356
+
357
+
358
+ def cmd_dumps_create(args):
359
+ config = _config(args)
360
+ client = make_client(config)
361
+ app_id = _require(args.app_id, config.app_id, "app-id (аргумент APP_ID или ELEMENT_APP_ID)")
362
+ _emit(client.create_dump(app_id, description=args.description or ""))
363
+ return 0
364
+
365
+
366
+ def cmd_dumps_get(args):
367
+ client = make_client(_config(args))
368
+ _emit(client.get_dump(args.app_id, args.dump_id))
369
+ return 0
370
+
371
+
372
+ def cmd_tasks_list(args):
373
+ client = make_client(_config(args))
374
+ _emit(client.list_app_tasks(args.app_id or ""))
375
+ return 0
376
+
377
+
378
+ def cmd_tasks_get_group(args):
379
+ client = make_client(_config(args))
380
+ _emit(client.get_group_task(args.task_id))
381
+ return 0
382
+
383
+
384
+ def cmd_tech_get(args):
385
+ config = _config(args)
386
+ client = make_client(config)
387
+ app_id = _require(args.app_id, config.app_id, "app-id (аргумент APP_ID или ELEMENT_APP_ID)")
388
+ _emit({"app-id": app_id, "technology-version": client.get_technology_version(app_id)})
389
+ return 0
390
+
391
+
392
+ def cmd_tech_set(args):
393
+ client = make_client(_config(args))
394
+ _emit(client.set_technology_version(args.version, [args.app_id]))
395
+ return 0
396
+
397
+
398
+ def cmd_mcp(args):
399
+ try:
400
+ from . import mcp_server
401
+ except ImportError:
402
+ raise ElemctlError(
403
+ 'MCP-зависимость не установлена – выполните: pip install "elemctl[mcp]"'
404
+ )
405
+ mcp_server.main()
406
+ return 0
407
+
408
+
409
+ # -- парсер --------------------------------------------------------------------
410
+
411
+
412
+ def build_parser():
413
+ parser = argparse.ArgumentParser(
414
+ prog="elemctl",
415
+ description="Управление приложениями платформы 1С:Предприятие.Элемент (Console API v2)",
416
+ )
417
+ parser.add_argument("--base-url", help="базовый URL платформы (ELEMENT_BASE_URL)")
418
+ parser.add_argument("--client-id", help="Client-Id для получения токена (ELEMENT_CLIENT_ID)")
419
+ parser.add_argument("--client-secret", help="Client-Secret (ELEMENT_CLIENT_SECRET)")
420
+ parser.add_argument("--env-file", help="путь к .env-файлу (по умолчанию .env в текущем каталоге)")
421
+ parser.add_argument("--timeout", type=float, default=None, help="таймаут запросов в секундах (по умолчанию 60)")
422
+ parser.add_argument("--version", action="version", version=f"elemctl {__version__}")
423
+
424
+ sub = parser.add_subparsers(dest="command", metavar="команда")
425
+
426
+ p = sub.add_parser("token", help="получить и напечатать токен")
427
+ p.set_defaults(handler=cmd_token)
428
+
429
+ # apps ----------------------------------------------------------------
430
+ apps = sub.add_parser("apps", help="приложения")
431
+ apps_sub = apps.add_subparsers(dest="subcommand", metavar="действие", required=True)
432
+
433
+ p = apps_sub.add_parser("list", help="список приложений")
434
+ p.add_argument("--name", help="фильтр по имени")
435
+ p.set_defaults(handler=cmd_apps_list)
436
+
437
+ p = apps_sub.add_parser("get", help="карточка приложения")
438
+ p.add_argument("app_id", nargs="?", metavar="APP_ID")
439
+ p.set_defaults(handler=cmd_apps_get)
440
+
441
+ p = apps_sub.add_parser("find", help="найти приложение по имени (точное совпадение без учёта регистра)")
442
+ p.add_argument("name", metavar="NAME")
443
+ p.set_defaults(handler=cmd_apps_find)
444
+
445
+ p = apps_sub.add_parser("create", help="создать приложение")
446
+ p.add_argument("name", metavar="NAME")
447
+ p.add_argument("--project-id", help="проект-источник")
448
+ p.add_argument("--version-id", help="id сборки-источника")
449
+ p.add_argument("--latest-build", action="store_true", help="источник – последняя сборка проекта")
450
+ p.add_argument("--space-id", help="пространство")
451
+ p.add_argument("--tech-version", help="версия технологии")
452
+ p.add_argument("--no-dev-mode", action="store_true", help="не создавать среду разработки")
453
+ p.add_argument("--wait", action="store_true", help="дождаться готовности и вывести карточку")
454
+ p.set_defaults(handler=cmd_apps_create)
455
+
456
+ p = apps_sub.add_parser("delete", help="удалить приложение (необратимо, URL меняется при пересоздании)")
457
+ p.add_argument("app_id", metavar="APP_ID")
458
+ p.set_defaults(handler=cmd_apps_delete)
459
+
460
+ p = apps_sub.add_parser("start", help="запустить приложение")
461
+ p.add_argument("app_id", nargs="?", metavar="APP_ID")
462
+ p.set_defaults(handler=cmd_apps_start)
463
+
464
+ p = apps_sub.add_parser("stop", help="остановить приложение")
465
+ p.add_argument("app_id", nargs="?", metavar="APP_ID")
466
+ p.set_defaults(handler=cmd_apps_stop)
467
+
468
+ # spaces ----------------------------------------------------------------
469
+ spaces = sub.add_parser("spaces", help="пространства")
470
+ spaces_sub = spaces.add_subparsers(dest="subcommand", metavar="действие", required=True)
471
+ p = spaces_sub.add_parser("list", help="список пространств")
472
+ p.set_defaults(handler=cmd_spaces_list)
473
+
474
+ # projects --------------------------------------------------------------
475
+ projects = sub.add_parser("projects", help="проекты")
476
+ projects_sub = projects.add_subparsers(dest="subcommand", metavar="действие", required=True)
477
+
478
+ p = projects_sub.add_parser("list", help="список проектов")
479
+ p.set_defaults(handler=cmd_projects_list)
480
+
481
+ p = projects_sub.add_parser("get", help="карточка проекта")
482
+ p.add_argument("project_id", nargs="?", metavar="PROJECT_ID")
483
+ p.set_defaults(handler=cmd_projects_get)
484
+
485
+ p = projects_sub.add_parser("delete", help="удалить проект")
486
+ p.add_argument("project_id", metavar="PROJECT_ID")
487
+ p.set_defaults(handler=cmd_projects_delete)
488
+
489
+ # builds ----------------------------------------------------------------
490
+ builds = sub.add_parser("builds", help="сборки проекта на платформе")
491
+ builds_sub = builds.add_subparsers(dest="subcommand", metavar="действие", required=True)
492
+
493
+ p = builds_sub.add_parser("list", help="список сборок проекта")
494
+ p.add_argument("--project-id")
495
+ p.set_defaults(handler=cmd_builds_list)
496
+
497
+ p = builds_sub.add_parser("get", help="карточка сборки по версии")
498
+ p.add_argument("version", metavar="VERSION")
499
+ p.add_argument("--project-id")
500
+ p.set_defaults(handler=cmd_builds_get)
501
+
502
+ p = builds_sub.add_parser("upload", help="загрузить файл сборки (.xasm/.xlib)")
503
+ p.add_argument("file", metavar="FILE")
504
+ p.add_argument("--project-id", help="проект (без него создаётся новый проект)")
505
+ p.add_argument("--space-id")
506
+ p.add_argument("--branch", help="имя git-ветки (метаданные)")
507
+ p.add_argument("--commit", help="хэш коммита (метаданные)")
508
+ p.add_argument("--commit-message", help="сообщение коммита (метаданные)")
509
+ p.set_defaults(handler=cmd_builds_upload)
510
+
511
+ p = builds_sub.add_parser("delete", help="удалить сборку по версии")
512
+ p.add_argument("version", metavar="VERSION")
513
+ p.add_argument("--project-id")
514
+ p.set_defaults(handler=cmd_builds_delete)
515
+
516
+ # build -----------------------------------------------------------------
517
+ p = sub.add_parser("build", help="локально собрать архив сборки из исходников")
518
+ p.add_argument("--project-dir", help="каталог проекта (по умолчанию ищется вглубь от текущего)")
519
+ p.add_argument("--output", help="каталог для архива (по умолчанию текущий)")
520
+ p.add_argument("--build-version", help="явная версия сборки, например 1.0-42")
521
+ p.add_argument("--last-build", help="версия последней сборки проекта – для автоинкремента")
522
+ p.add_argument("--commit", help="хэш коммита в манифест (по умолчанию из git)")
523
+ p.add_argument("--branch", help="имя ветки в манифест (по умолчанию из git)")
524
+ p.add_argument("--kind", choices=["application", "library"], help="вид проекта (по умолчанию из Проект.yaml)")
525
+ p.set_defaults(handler=cmd_build)
526
+
527
+ # deploy ----------------------------------------------------------------
528
+ p = sub.add_parser(
529
+ "deploy",
530
+ help="полный цикл: сборка -> загрузка -> применение -> перезапуск -> проверка применения",
531
+ )
532
+ p.add_argument("--app-id")
533
+ p.add_argument("--project-id")
534
+ p.add_argument("--project-dir")
535
+ p.add_argument("--output", help="каталог для архива (по умолчанию временный)")
536
+ p.add_argument("--build-version", help="явная версия сборки")
537
+ p.add_argument("--branch", help="имя ветки в метаданные (по умолчанию из git)")
538
+ p.add_argument("--commit", help="хэш коммита в метаданные (по умолчанию из git)")
539
+ p.add_argument("--commit-message", help="сообщение коммита (метаданные загрузки)")
540
+ p.add_argument("--from-branch", action="store_true", help="вместо сборки исходников – загрузка из git-ветки платформы")
541
+ p.add_argument("--branch-id", help="ветка платформы для --from-branch (или ELEMENT_BRANCH_ID)")
542
+ p.add_argument("--dry-run", action="store_true", help="только сборка, без загрузки")
543
+ p.set_defaults(handler=cmd_deploy)
544
+
545
+ # branches ----------------------------------------------------------------
546
+ branches = sub.add_parser("branches", help="ветки среды разработки")
547
+ branches_sub = branches.add_subparsers(dest="subcommand", metavar="действие", required=True)
548
+
549
+ p = branches_sub.add_parser("list", help="список веток")
550
+ p.add_argument("--project-id")
551
+ p.add_argument("--name")
552
+ p.set_defaults(handler=cmd_branches_list)
553
+
554
+ p = branches_sub.add_parser("get", help="карточка ветки")
555
+ p.add_argument("branch_id", metavar="ID")
556
+ p.set_defaults(handler=cmd_branches_get)
557
+
558
+ p = branches_sub.add_parser("create", help="создать ветку")
559
+ p.add_argument("name", metavar="NAME")
560
+ p.add_argument("--project-id")
561
+ p.add_argument("--app-id", help="сразу привязать к приложению")
562
+ p.set_defaults(handler=cmd_branches_create)
563
+
564
+ p = branches_sub.add_parser("update", help="изменить ветку (перепривязать к приложению)")
565
+ p.add_argument("branch_id", metavar="ID")
566
+ p.add_argument("--app-id")
567
+ p.set_defaults(handler=cmd_branches_update)
568
+
569
+ p = branches_sub.add_parser("delete", help="удалить ветку")
570
+ p.add_argument("branch_id", metavar="ID")
571
+ p.set_defaults(handler=cmd_branches_delete)
572
+
573
+ p = branches_sub.add_parser("merge", help="принять изменения ветки")
574
+ p.add_argument("branch_id", metavar="ID")
575
+ p.set_defaults(handler=cmd_branches_merge)
576
+
577
+ p = branches_sub.add_parser(
578
+ "sync", help="загрузить изменения из git-ветки платформы в приложение (внутренний UI API)"
579
+ )
580
+ p.add_argument("branch_id", nargs="?", metavar="ID")
581
+ p.add_argument("--app-id")
582
+ p.set_defaults(handler=cmd_branches_sync)
583
+
584
+ # dumps ----------------------------------------------------------------
585
+ dumps = sub.add_parser("dumps", help="дампы приложений")
586
+ dumps_sub = dumps.add_subparsers(dest="subcommand", metavar="действие", required=True)
587
+
588
+ p = dumps_sub.add_parser("create", help="создать дамп")
589
+ p.add_argument("app_id", nargs="?", metavar="APP_ID")
590
+ p.add_argument("--description", help="описание дампа")
591
+ p.set_defaults(handler=cmd_dumps_create)
592
+
593
+ p = dumps_sub.add_parser("get", help="статус дампа")
594
+ p.add_argument("app_id", metavar="APP_ID")
595
+ p.add_argument("dump_id", metavar="DUMP_ID")
596
+ p.set_defaults(handler=cmd_dumps_get)
597
+
598
+ # tasks ----------------------------------------------------------------
599
+ tasks = sub.add_parser("tasks", help="задачи приложений")
600
+ tasks_sub = tasks.add_subparsers(dest="subcommand", metavar="действие", required=True)
601
+
602
+ p = tasks_sub.add_parser("list", help="список задач приложений")
603
+ p.add_argument("--app-id", help="фильтр по приложению (на клиенте)")
604
+ p.set_defaults(handler=cmd_tasks_list)
605
+
606
+ p = tasks_sub.add_parser("get-group", help="статус групповой задачи")
607
+ p.add_argument("task_id", metavar="TASK_ID")
608
+ p.set_defaults(handler=cmd_tasks_get_group)
609
+
610
+ # tech ----------------------------------------------------------------
611
+ tech = sub.add_parser("tech", help="версия технологии")
612
+ tech_sub = tech.add_subparsers(dest="subcommand", metavar="действие", required=True)
613
+
614
+ p = tech_sub.add_parser("get", help="версия технологии приложения")
615
+ p.add_argument("app_id", nargs="?", metavar="APP_ID")
616
+ p.set_defaults(handler=cmd_tech_get)
617
+
618
+ p = tech_sub.add_parser("set", help="обновить версию технологии (групповая задача)")
619
+ p.add_argument("app_id", metavar="APP_ID")
620
+ p.add_argument("version", metavar="VERSION")
621
+ p.set_defaults(handler=cmd_tech_set)
622
+
623
+ # mcp ----------------------------------------------------------------
624
+ p = sub.add_parser("mcp", help="запустить MCP-сервер (транспорт stdio)")
625
+ p.set_defaults(handler=cmd_mcp)
626
+
627
+ return parser
628
+
629
+
630
+ def main(argv=None):
631
+ """Точка входа CLI; возвращает код завершения процесса."""
632
+ _reconfigure_streams()
633
+ parser = build_parser()
634
+ args = parser.parse_args(argv)
635
+ handler = getattr(args, "handler", None)
636
+ if handler is None:
637
+ parser.print_help(sys.stderr)
638
+ return 1
639
+ try:
640
+ result = handler(args)
641
+ return 0 if result is None else int(result)
642
+ except ApiError as error:
643
+ return _fail(error.to_dict())
644
+ except ElemctlError as error:
645
+ return _fail({"error": str(error)})
646
+ except OSError as error:
647
+ return _fail({"error": str(error)})
648
+
649
+
650
+ if __name__ == "__main__":
651
+ sys.exit(main())