homeconsole-cli 0.0.1__tar.gz → 0.0.2__tar.gz

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.
Files changed (65) hide show
  1. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/PKG-INFO +30 -1
  2. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/README.md +28 -0
  3. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/deploy.py +208 -11
  4. homeconsole_cli-0.0.2/hc/commands/env.py +588 -0
  5. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/install.py +5 -0
  6. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/remove.py +14 -0
  7. homeconsole_cli-0.0.2/hc/main.py +210 -0
  8. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/homeconsole_cli.egg-info/PKG-INFO +30 -1
  9. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/homeconsole_cli.egg-info/SOURCES.txt +2 -0
  10. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/homeconsole_cli.egg-info/requires.txt +1 -0
  11. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/pyproject.toml +2 -1
  12. homeconsole_cli-0.0.2/tests/test_nav_cli.py +34 -0
  13. homeconsole_cli-0.0.1/hc/main.py +0 -105
  14. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/__init__.py +0 -0
  15. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/api.py +0 -0
  16. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/capabilities.py +0 -0
  17. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/client.py +0 -0
  18. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/__init__.py +0 -0
  19. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/_client_helpers.py +0 -0
  20. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/_compose_helpers.py +0 -0
  21. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/auth.py +0 -0
  22. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/connect.py +0 -0
  23. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/core.py +0 -0
  24. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/logs.py +0 -0
  25. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/marketplace.py +0 -0
  26. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/module.py +0 -0
  27. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/ping.py +0 -0
  28. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/plugin.py +0 -0
  29. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/__init__.py +0 -0
  30. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/compose.py +0 -0
  31. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/config.py +0 -0
  32. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/core.py +0 -0
  33. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/db.py +0 -0
  34. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/mode.py +0 -0
  35. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/redis.py +0 -0
  36. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/recovery/ui.py +0 -0
  37. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/reset.py +0 -0
  38. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/search.py +0 -0
  39. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/secrets.py +0 -0
  40. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/setup.py +0 -0
  41. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/setup_wizard.py +0 -0
  42. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/status.py +0 -0
  43. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/commands/update.py +0 -0
  44. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/config.py +0 -0
  45. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/constants.py +0 -0
  46. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/core_ops.py +0 -0
  47. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/core_source.py +0 -0
  48. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/env_bootstrap.py +0 -0
  49. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/errors.py +0 -0
  50. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/marketplace_operation.py +0 -0
  51. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/native_core.py +0 -0
  52. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/repl.py +0 -0
  53. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/setup_runner.py +0 -0
  54. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/hc/shell.py +0 -0
  55. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/homeconsole_cli.egg-info/dependency_links.txt +0 -0
  56. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/homeconsole_cli.egg-info/entry_points.txt +0 -0
  57. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/homeconsole_cli.egg-info/top_level.txt +0 -0
  58. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/setup.cfg +0 -0
  59. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_config_roundtrip.py +0 -0
  60. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_core_ops.py +0 -0
  61. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_env_bootstrap.py +0 -0
  62. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_main_root_callback.py +0 -0
  63. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_marketplace_operation_parse.py +0 -0
  64. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_native_core_helpers.py +0 -0
  65. {homeconsole_cli-0.0.1 → homeconsole_cli-0.0.2}/tests/test_setup_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: homeconsole-cli
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: HomeConsole CLI (hc) — управление платформой через HTTP API
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -10,6 +10,7 @@ Requires-Dist: httpx>=0.27
10
10
  Requires-Dist: anyio>=4
11
11
  Requires-Dist: prompt_toolkit>=3
12
12
  Requires-Dist: tomlkit>=0.12
13
+ Requires-Dist: questionary>=2.0
13
14
  Provides-Extra: dev
14
15
  Requires-Dist: pytest>=8; extra == "dev"
15
16
 
@@ -31,6 +32,7 @@ pip install -e .
31
32
 
32
33
  ```bash
33
34
  hc --help
35
+ hc nav
34
36
  ```
35
37
 
36
38
  ## Конфигурация
@@ -89,8 +91,35 @@ hc plugin list
89
91
  - `hc deploy` (по умолчанию: build+push+rollout+wait) и `hc deploy ...` (тонкие подкоманды)
90
92
  - `hc deploy platform` (локальный dev flow)
91
93
  - `hc deploy platform --mode image --image ghcr.io/home-console/platform-home-console --tag latest` (image-only deploy)
94
+ - `hc deploy stack dev` / `hc deploy stack prod` (полный stack для dev/prod)
92
95
  - `hc update core ...` (обновление core-runtime до нового image:tag)
93
96
  - `hc shell`
97
+ - `hc nav [section ...]` (удобная навигация по командам: `hc nav deploy`, `hc nav deploy dev`)
98
+
99
+ ### Навигация по командам (без запоминания)
100
+
101
+ ```bash
102
+ hc nav
103
+ hc nav deploy
104
+ hc nav deploy dev
105
+ ```
106
+
107
+ Подсказка: на любом уровне можно проваливаться дальше и смотреть доступные разделы.
108
+
109
+ ### Быстрый dev up по профилям
110
+
111
+ ```bash
112
+ hc deploy dev up --profile base
113
+ hc deploy dev up --profile platform
114
+ hc deploy dev up --profile cache
115
+ hc deploy dev up --profile db
116
+ ```
117
+
118
+ Профили:
119
+ - `base` = `core+proxy`
120
+ - `platform` = `core+proxy+platform`
121
+ - `cache` = `core+proxy+platform+cache`
122
+ - `db` = `core+proxy+platform+cache+db`
94
123
 
95
124
  ### Deploy “одной командой”
96
125
 
@@ -16,6 +16,7 @@ pip install -e .
16
16
 
17
17
  ```bash
18
18
  hc --help
19
+ hc nav
19
20
  ```
20
21
 
21
22
  ## Конфигурация
@@ -74,8 +75,35 @@ hc plugin list
74
75
  - `hc deploy` (по умолчанию: build+push+rollout+wait) и `hc deploy ...` (тонкие подкоманды)
75
76
  - `hc deploy platform` (локальный dev flow)
76
77
  - `hc deploy platform --mode image --image ghcr.io/home-console/platform-home-console --tag latest` (image-only deploy)
78
+ - `hc deploy stack dev` / `hc deploy stack prod` (полный stack для dev/prod)
77
79
  - `hc update core ...` (обновление core-runtime до нового image:tag)
78
80
  - `hc shell`
81
+ - `hc nav [section ...]` (удобная навигация по командам: `hc nav deploy`, `hc nav deploy dev`)
82
+
83
+ ### Навигация по командам (без запоминания)
84
+
85
+ ```bash
86
+ hc nav
87
+ hc nav deploy
88
+ hc nav deploy dev
89
+ ```
90
+
91
+ Подсказка: на любом уровне можно проваливаться дальше и смотреть доступные разделы.
92
+
93
+ ### Быстрый dev up по профилям
94
+
95
+ ```bash
96
+ hc deploy dev up --profile base
97
+ hc deploy dev up --profile platform
98
+ hc deploy dev up --profile cache
99
+ hc deploy dev up --profile db
100
+ ```
101
+
102
+ Профили:
103
+ - `base` = `core+proxy`
104
+ - `platform` = `core+proxy+platform`
105
+ - `cache` = `core+proxy+platform+cache`
106
+ - `db` = `core+proxy+platform+cache+db`
79
107
 
80
108
  ### Deploy “одной командой”
81
109
 
@@ -34,6 +34,22 @@ _MODE_HELP = (
34
34
  "алиас image → dev-image)"
35
35
  )
36
36
  _DEPLOY_MODE_HELP = _MODE_HELP
37
+ _DEV_PROFILE_SERVICES: dict[str, list[str]] = {
38
+ "core+proxy": ["core-runtime", "caddy"],
39
+ "core+proxy+platform": ["core-runtime", "caddy", "platform-web"],
40
+ "core+proxy+platform+cache": ["core-runtime", "caddy", "platform-web", "redis"],
41
+ "core+proxy+platform+cache+db": ["core-runtime", "caddy", "platform-web", "redis", "postgres"],
42
+ }
43
+ _DEV_PROFILE_ALIASES: dict[str, str] = {
44
+ "base": "core+proxy",
45
+ "platform": "core+proxy+platform",
46
+ "cache": "core+proxy+platform+cache",
47
+ "db": "core+proxy+platform+cache+db",
48
+ }
49
+ _STACK_ENV_COMPOSE: dict[str, str] = {
50
+ "dev": "deploy/dev/docker-compose.image.yml",
51
+ "prod": "deploy/prod/docker-compose.image.yml",
52
+ }
37
53
 
38
54
 
39
55
  def _find_repo_root() -> Path | None:
@@ -125,6 +141,34 @@ def _normalize_edge_health_path(p: str) -> str:
125
141
  return v
126
142
 
127
143
 
144
+ def _resolve_dev_profile(profile: str) -> tuple[str, list[str]]:
145
+ raw = (profile or "").strip().lower()
146
+ resolved = _DEV_PROFILE_ALIASES.get(raw, raw)
147
+ if resolved not in _DEV_PROFILE_SERVICES:
148
+ choices = " | ".join(
149
+ [*sorted(_DEV_PROFILE_SERVICES.keys()), *sorted(_DEV_PROFILE_ALIASES.keys())]
150
+ )
151
+ raise InvalidModeError(
152
+ message=f"--profile {profile!r} недопустим.",
153
+ exit_code=2,
154
+ hint=f"Допустимые профили: {choices}",
155
+ )
156
+ return resolved, _DEV_PROFILE_SERVICES[resolved]
157
+
158
+
159
+ def _resolve_stack_env(env: str | None, compose_rel: str) -> tuple[str, str]:
160
+ raw = (env or "prod").strip().lower()
161
+ if raw not in _STACK_ENV_COMPOSE:
162
+ raise InvalidModeError(
163
+ message=f"stack env {env!r} недопустим.",
164
+ exit_code=2,
165
+ hint="Допустимые: dev | prod",
166
+ )
167
+ default_compose = _STACK_ENV_COMPOSE[raw]
168
+ resolved_compose = compose_rel if compose_rel.strip() else default_compose
169
+ return raw, resolved_compose
170
+
171
+
128
172
  def _wait_http_ok(
129
173
  *,
130
174
  url: str,
@@ -434,6 +478,7 @@ def register(app: typer.Typer) -> None:
434
478
  ),
435
479
  quiet: bool = typer.Option(False, "--quiet", help="Минимальный вывод (только итог/ошибка)"),
436
480
  json_out: bool = typer.Option(False, "--json", help="Машинный вывод в JSON"),
481
+ dry_run: bool = typer.Option(False, "--dry-run", help="Показать план деплоя без реального выполнения"),
437
482
  ) -> None:
438
483
  """
439
484
  Если запущено как `hc deploy` без подкоманд — выполняет полный пайплайн:
@@ -469,6 +514,23 @@ def register(app: typer.Typer) -> None:
469
514
  Panel.fit(f"{full}\nmode={resolved_mode}\ntarget={target}", title="hc deploy")
470
515
  )
471
516
 
517
+ if dry_run:
518
+ from rich.table import Table as _Table
519
+ plan = _Table(title="Dry run — план деплоя")
520
+ plan.add_column("Шаг", style="bold")
521
+ plan.add_column("Действие")
522
+ if build:
523
+ plan.add_row("Build", f"docker build -t {full} <src>")
524
+ if push:
525
+ plan.add_row("Push", f"docker push {full}")
526
+ if rollout:
527
+ plan.add_row("Rollout", f"compose pull + up -d → {target}")
528
+ if rollout and wait:
529
+ plan.add_row("Wait healthy", f"{health_url} (timeout={timeout}s)")
530
+ console.print(plan)
531
+ console.print("[yellow]Dry run:[/yellow] деплой не выполнен")
532
+ raise typer.Exit(code=0)
533
+
472
534
  if build:
473
535
  src = _resolve_source(console)
474
536
  t0 = _step_start(console, f"Build {full}", quiet=quiet or json_out)
@@ -591,6 +653,7 @@ def register(app: typer.Typer) -> None:
591
653
  "--restart-remote/--no-restart-remote",
592
654
  help="После remote sync перезапустить caddy (docker compose restart)",
593
655
  ),
656
+ dry_run: bool = typer.Option(False, "--dry-run", help="Показать план деплоя без реального выполнения"),
594
657
  ) -> None:
595
658
  """Локально собрать platform web или запустить platform image из GHCR."""
596
659
  console = Console()
@@ -606,6 +669,31 @@ def register(app: typer.Typer) -> None:
606
669
  else _resolve_platform_root(console)
607
670
  )
608
671
 
672
+ if dry_run:
673
+ from rich.table import Table as _Table
674
+ plan = _Table(title="Dry run — план деплоя platform")
675
+ plan.add_column("Шаг", style="bold")
676
+ plan.add_column("Действие")
677
+ plan.add_row("Режим", resolved_mode)
678
+ plan.add_row("Platform root", str(platform_root))
679
+ if resolved_mode == "image":
680
+ plan.add_row("Image", f"{image}:{tag}")
681
+ if start:
682
+ plan.add_row("Start", "docker compose pull + up -d")
683
+ else:
684
+ if build:
685
+ plan.add_row("Build", "pnpm --filter=web build")
686
+ plan.add_row("Sync dist", str(platform_root / "apps" / "web" / "dist"))
687
+ if ssh:
688
+ plan.add_row("Remote sync", f"rsync dist → {ssh}:{path}/deploy/dev/frontend/")
689
+ if restart_remote:
690
+ plan.add_row("Restart caddy", f"docker compose restart caddy на {ssh}")
691
+ elif start:
692
+ plan.add_row("Start core", "bash deploy/dev/start.sh")
693
+ console.print(plan)
694
+ console.print("[yellow]Dry run:[/yellow] деплой не выполнен")
695
+ raise typer.Exit(code=0)
696
+
609
697
  if resolved_mode == "image":
610
698
  require_docker(console)
611
699
  compose_file = platform_root / "docker-compose.image.yml"
@@ -702,6 +790,10 @@ def register(app: typer.Typer) -> None:
702
790
 
703
791
  @deploy_app.command("stack")
704
792
  def deploy_stack(
793
+ env: str = typer.Argument(
794
+ "prod",
795
+ help="Окружение стека: dev | prod (пример: `hc deploy stack dev`)",
796
+ ),
705
797
  core_image: str | None = typer.Option(
706
798
  None, "--core-image", help="Core image без тега (по умолчанию из deploy.core_image)"
707
799
  ),
@@ -721,9 +813,9 @@ def register(app: typer.Typer) -> None:
721
813
  help="remote path к core-runtime-service (по умолчанию из deploy.path)",
722
814
  ),
723
815
  compose_rel: str = typer.Option(
724
- "deploy/prod/docker-compose.image.yml",
816
+ "",
725
817
  "--compose",
726
- help="Путь к compose (относительно core-runtime-service на сервере)",
818
+ help="Путь к compose (если не указан: выбирается по env: dev/prod)",
727
819
  ),
728
820
  core_runtime_url: str = typer.Option(
729
821
  "http://core-runtime:8000",
@@ -788,6 +880,7 @@ def register(app: typer.Typer) -> None:
788
880
  interval: float = typer.Option(1.0, "--interval", help="Интервал проверки healthy (сек)"),
789
881
  quiet: bool = typer.Option(False, "--quiet", help="Минимальный вывод"),
790
882
  json_out: bool = typer.Option(False, "--json", help="Машинный вывод в JSON"),
883
+ dry_run: bool = typer.Option(False, "--dry-run", help="Показать план деплоя без реального выполнения"),
791
884
  ) -> None:
792
885
  """
793
886
  Деплой всего стека (core-runtime + platform-web) из image’ов.
@@ -800,6 +893,7 @@ def register(app: typer.Typer) -> None:
800
893
  require_docker(console)
801
894
  cfg = Config.load()
802
895
  src = _resolve_source(console)
896
+ resolved_env, resolved_compose_rel = _resolve_stack_env(env, compose_rel)
803
897
 
804
898
  resolved_core_image = (core_image or cfg.deploy.core_image).strip()
805
899
  full_core = f"{resolved_core_image}:{core_tag}"
@@ -831,18 +925,41 @@ def register(app: typer.Typer) -> None:
831
925
  if not quiet and not json_out:
832
926
  console.print(
833
927
  Panel.fit(
834
- f"core={full_core}\nplatform={full_platform}\ncompose={compose_rel}\n"
928
+ f"core={full_core}\nplatform={full_platform}\ncompose={resolved_compose_rel}\n"
929
+ f"env={resolved_env}\n"
835
930
  f"target={'remote ' + resolved_ssh if resolved_ssh else 'local'}",
836
931
  title="hc deploy stack",
837
932
  )
838
933
  )
839
934
 
935
+ if dry_run:
936
+ from rich.table import Table as _Table
937
+ target_str = f"remote {resolved_ssh}" if resolved_ssh else "local"
938
+ plan = _Table(title="Dry run — план деплоя stack")
939
+ plan.add_column("Шаг", style="bold")
940
+ plan.add_column("Действие")
941
+ plan.add_row("Target", target_str)
942
+ plan.add_row("Env", resolved_env)
943
+ plan.add_row("Core image", full_core)
944
+ plan.add_row("Platform image", full_platform)
945
+ plan.add_row("Compose", resolved_compose_rel)
946
+ if pull:
947
+ plan.add_row("Pull", "docker compose pull core-runtime platform-web edge")
948
+ plan.add_row("Rollout", "docker compose up -d")
949
+ if wait:
950
+ plan.add_row("Wait healthy", f"edge → core ({edge_health_path}, timeout={timeout}s)")
951
+ if (external_base_url or "").strip():
952
+ plan.add_row("External check", (external_base_url or "").strip())
953
+ console.print(plan)
954
+ console.print("[yellow]Dry run:[/yellow] деплой не выполнен")
955
+ raise typer.Exit(code=0)
956
+
840
957
  if resolved_ssh:
841
958
  if not resolved_path:
842
959
  raise HcCliError(
843
960
  message="для --ssh нужен --path",
844
961
  exit_code=2,
845
- hint="Пример: `hc deploy stack --ssh user@host --path /srv/core-runtime-service`",
962
+ hint=f"Пример: `hc deploy stack {resolved_env} --ssh user@host --path /srv/core-runtime-service`",
846
963
  )
847
964
  t0 = _step_start(
848
965
  console, "Rollout stack remote (compose pull + up -d)", quiet=quiet or json_out
@@ -851,9 +968,9 @@ def register(app: typer.Typer) -> None:
851
968
  remote = f"cd {shlex.quote(resolved_path)} && "
852
969
  if pull:
853
970
  remote += (
854
- f"{exports} docker compose -f {shlex.quote(compose_rel)} pull core-runtime platform-web edge && "
971
+ f"{exports} docker compose -f {shlex.quote(resolved_compose_rel)} pull core-runtime platform-web edge && "
855
972
  )
856
- remote += f"{exports} docker compose -f {shlex.quote(compose_rel)} up -d"
973
+ remote += f"{exports} docker compose -f {shlex.quote(resolved_compose_rel)} up -d"
857
974
  _run(_ssh_cmd(resolved_ssh, remote))
858
975
  dt = _step_ok(console, "Rollout", t0, quiet=quiet or json_out)
859
976
  steps.append({"name": "rollout", "ok": True, "duration_s": dt})
@@ -868,9 +985,9 @@ def register(app: typer.Typer) -> None:
868
985
  while time.time() < deadline:
869
986
  chk = (
870
987
  f"cd {shlex.quote(resolved_path)} && "
871
- f"docker compose -f {shlex.quote(compose_rel)} exec -T edge sh -lc "
988
+ f"docker compose -f {shlex.quote(resolved_compose_rel)} exec -T edge sh -lc "
872
989
  f"{shlex.quote(f'curl -fsS http://127.0.0.1{edge_health_path} >/dev/null && echo edge_core_ok || echo edge_core_no')} && "
873
- f"docker compose -f {shlex.quote(compose_rel)} exec -T edge sh -lc "
990
+ f"docker compose -f {shlex.quote(resolved_compose_rel)} exec -T edge sh -lc "
874
991
  f"{shlex.quote('wget -qO- http://127.0.0.1/ >/dev/null && echo edge_ui_ok || echo edge_ui_no')}"
875
992
  )
876
993
  p = subprocess.run(_ssh_cmd(resolved_ssh, chk), text=True, capture_output=True, check=False) # noqa: S603
@@ -892,12 +1009,12 @@ def register(app: typer.Typer) -> None:
892
1009
  steps.append({"name": "wait", "ok": True, "duration_s": dt})
893
1010
  else:
894
1011
  # local: apply compose from repo source (core-runtime-service)
895
- compose_file = src.path / compose_rel
1012
+ compose_file = src.path / resolved_compose_rel
896
1013
  if not compose_file.exists():
897
1014
  raise HcCliError(
898
1015
  message=f"Не найден compose файл: {compose_file}",
899
1016
  exit_code=1,
900
- hint="Проверь путь `--compose` и наличие deploy/prod/docker-compose.image.yml в core-runtime-service.",
1017
+ hint="Проверь `--compose` или используй `hc deploy stack dev|prod`.",
901
1018
  )
902
1019
  env = {**os.environ, **env_pairs}
903
1020
  t0 = _step_start(console, "Rollout stack local (compose pull + up -d)", quiet=quiet or json_out)
@@ -1031,7 +1148,8 @@ def register(app: typer.Typer) -> None:
1031
1148
  "command": "deploy.stack",
1032
1149
  "core": full_core,
1033
1150
  "platform": full_platform,
1034
- "compose": compose_rel,
1151
+ "env": resolved_env,
1152
+ "compose": resolved_compose_rel,
1035
1153
  "target": f"remote {resolved_ssh}" if resolved_ssh else "local",
1036
1154
  "wait": bool(wait),
1037
1155
  "timeout_s": int(timeout),
@@ -1061,6 +1179,84 @@ def register(app: typer.Typer) -> None:
1061
1179
  console.print(f"[dim]Подсказка:[/dim] {e.hint}")
1062
1180
  raise typer.Exit(code=int(e.exit_code or 1))
1063
1181
 
1182
+ dev_app = typer.Typer(
1183
+ help="Dev stack shortcuts (up/down профили сервисов)",
1184
+ context_settings={"help_option_names": ["-h", "--help"]},
1185
+ no_args_is_help=True,
1186
+ )
1187
+
1188
+ @dev_app.command("up")
1189
+ def dev_up(
1190
+ profile: str = typer.Option(
1191
+ "core+proxy",
1192
+ "--profile",
1193
+ help=(
1194
+ "Профиль сервисов: core+proxy | core+proxy+platform | "
1195
+ "core+proxy+platform+cache | core+proxy+platform+cache+db "
1196
+ "(алиасы: base|platform|cache|db)"
1197
+ ),
1198
+ ),
1199
+ ssh: str | None = typer.Option(
1200
+ None, "--ssh", help="user@host для удалённого запуска (по умолчанию из deploy.ssh)"
1201
+ ),
1202
+ path: str | None = typer.Option(
1203
+ None, "--path", help="remote path к core-runtime-service (для --ssh)"
1204
+ ),
1205
+ pull: bool = typer.Option(
1206
+ False,
1207
+ "--pull/--no-pull",
1208
+ help="Перед up сделать docker compose pull для сервисов профиля",
1209
+ ),
1210
+ ) -> None:
1211
+ """Поднять dev compose только с нужным набором сервисов. (Используй `hc env up` для hot-reload.)"""
1212
+ console = Console()
1213
+ try:
1214
+ require_docker(console)
1215
+ src = _resolve_source(console)
1216
+ cfg = Config.load()
1217
+ resolved_profile, services = _resolve_dev_profile(profile)
1218
+ resolved_ssh = ssh if ssh is not None else (cfg.deploy.ssh or None)
1219
+ resolved_path = path if path is not None else (cfg.deploy.path or None)
1220
+
1221
+ compose_rel = src.compose_rel("dev")
1222
+ service_list = " ".join(services)
1223
+ console.print(f"[cyan]→[/cyan] dev up profile=[bold]{resolved_profile}[/bold] services={service_list}")
1224
+
1225
+ if resolved_ssh:
1226
+ if not resolved_path:
1227
+ console.print("[red]Ошибка:[/red] для --ssh нужен --path")
1228
+ raise typer.Exit(code=2)
1229
+ pull_cmd = (
1230
+ f"docker compose -f {shlex.quote(compose_rel)} pull {' '.join(shlex.quote(s) for s in services)} && "
1231
+ if pull
1232
+ else ""
1233
+ )
1234
+ remote = (
1235
+ f"cd {shlex.quote(resolved_path)} && "
1236
+ f"{pull_cmd}"
1237
+ f"docker compose -f {shlex.quote(compose_rel)} up -d {' '.join(shlex.quote(s) for s in services)}"
1238
+ )
1239
+ _run(_ssh_cmd(resolved_ssh, remote))
1240
+ console.print("[green]✓[/green] remote dev profile started")
1241
+ return
1242
+
1243
+ project = compose_project_from_source(console, src, mode="dev")
1244
+ if pull:
1245
+ _run(
1246
+ ["docker", "compose", "-f", str(project.compose_file), "pull", *services],
1247
+ cwd=project.cwd,
1248
+ )
1249
+ _run(
1250
+ ["docker", "compose", "-f", str(project.compose_file), "up", "-d", *services],
1251
+ cwd=project.cwd,
1252
+ )
1253
+ console.print("[green]✓[/green] local dev profile started")
1254
+ except HcCliError as e:
1255
+ console.print(f"[red]Ошибка:[/red] {e.message}")
1256
+ if e.hint:
1257
+ console.print(f"[dim]Подсказка:[/dim] {e.hint}")
1258
+ raise typer.Exit(code=int(e.exit_code or 1))
1259
+
1064
1260
  cfg_app = typer.Typer(
1065
1261
  help="Дефолты для deploy (ssh/path/image/mode)",
1066
1262
  context_settings={"help_option_names": ["-h", "--help"]},
@@ -1463,4 +1659,5 @@ def register(app: typer.Typer) -> None:
1463
1659
 
1464
1660
  deploy_app.add_typer(cfg_app, name="config")
1465
1661
  deploy_app.add_typer(core_app, name="core")
1662
+ deploy_app.add_typer(dev_app, name="dev")
1466
1663
  app.add_typer(deploy_app, name="deploy")