magicbox-dk 0.0.2.dev1__tar.gz → 0.0.3__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 (140) hide show
  1. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/.bumpversion.toml +3 -3
  2. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/CLAUDE.md +8 -6
  3. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/PKG-INFO +62 -1
  4. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/README.md +61 -0
  5. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/_base.py +17 -0
  6. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/dashboard.py +2 -2
  7. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/prepare.py +52 -1
  8. magicbox_dk-0.0.2.dev1/cli/commands/update.py → magicbox_dk-0.0.3/cli/commands/pull.py +4 -4
  9. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/start.py +5 -4
  10. magicbox_dk-0.0.3/cli/commands/update.py +140 -0
  11. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/compose/__init__.py +9 -0
  12. magicbox_dk-0.0.3/cli/compose/build_state.py +281 -0
  13. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/compose/builder.py +12 -9
  14. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/compose/healthcheck.py +7 -40
  15. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/compose/network.py +63 -12
  16. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/compose/runtime.py +39 -21
  17. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/core/context.py +3 -0
  18. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/core/paths.py +7 -0
  19. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/main.py +39 -4
  20. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/docker.py +26 -2
  21. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/settings.py +4 -0
  22. magicbox_dk-0.0.3/cli/version_check.py +149 -0
  23. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/back.config.yml +7 -4
  24. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/pyproject.toml +1 -1
  25. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/uv.lock +1 -1
  26. magicbox_dk-0.0.2.dev1/cli/commands/version.py +0 -30
  27. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/.github/workflows/ci.yml +0 -0
  28. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/.github/workflows/cleanup-prereleases.yml +0 -0
  29. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/.github/workflows/release.yml +0 -0
  30. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/.gitignore +0 -0
  31. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/LICENSE +0 -0
  32. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/__init__.py +0 -0
  33. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/cleanup.py +0 -0
  34. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/__init__.py +0 -0
  35. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/clean.py +0 -0
  36. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/create.py +0 -0
  37. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/credentials.py +0 -0
  38. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/delete.py +0 -0
  39. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/doctor.py +0 -0
  40. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/edit.py +0 -0
  41. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/env.py +0 -0
  42. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/init.py +0 -0
  43. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/list.py +0 -0
  44. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/logs.py +0 -0
  45. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/restart.py +0 -0
  46. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/settings.py +0 -0
  47. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/shell.py +0 -0
  48. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/show.py +0 -0
  49. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/status.py +0 -0
  50. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/stop.py +0 -0
  51. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/switch.py +0 -0
  52. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/commands/validate.py +0 -0
  53. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/compose/templates.py +0 -0
  54. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/core/__init__.py +0 -0
  55. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/core/cmd.py +0 -0
  56. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/core/profile_io.py +0 -0
  57. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/debug.py +0 -0
  58. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/__init__.py +0 -0
  59. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/_readiness.py +0 -0
  60. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/_state.py +0 -0
  61. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/env.py +0 -0
  62. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/frontend.py +0 -0
  63. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/infra.py +0 -0
  64. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/kin.py +0 -0
  65. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/profile.py +0 -0
  66. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/profile_meta.py +0 -0
  67. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/runnable.py +0 -0
  68. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/editors/settings.py +0 -0
  69. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/env/__init__.py +0 -0
  70. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/env/dotenv.py +0 -0
  71. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/env/loaders.py +0 -0
  72. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/env/regenerate.py +0 -0
  73. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/env/resolver.py +0 -0
  74. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/models/__init__.py +0 -0
  75. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/models/env.py +0 -0
  76. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/models/profile.py +0 -0
  77. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/models/service.py +0 -0
  78. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/__init__.py +0 -0
  79. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/base.py +0 -0
  80. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/binaries.py +0 -0
  81. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/git.py +0 -0
  82. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/process.py +0 -0
  83. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/runner/redaction.py +0 -0
  84. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/cli/ui.py +0 -0
  85. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/credentials.base.yaml +0 -0
  86. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/globals.base.yaml +0 -0
  87. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/profile.base.yaml +0 -0
  88. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/chainlit.config.yml +0 -0
  89. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/horizon.config.yml +0 -0
  90. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/module.config.yml +0 -0
  91. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/node-service-provider.config.yml +0 -0
  92. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/s3.config.yml +0 -0
  93. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/services/surrealdb.config.yml +0 -0
  94. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/configs/settings.base.yaml +0 -0
  95. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/archetypes/docker-compose.yml +0 -0
  96. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/back/docker-compose.yml +0 -0
  97. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/chainlit/docker-compose.yml +0 -0
  98. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/docker-compose.yml +0 -0
  99. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/horizon/docker-compose.yml +0 -0
  100. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/node-service-provider/docker-compose.yml +0 -0
  101. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/redis/docker-compose.yml +0 -0
  102. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/s3/docker-compose.yml +0 -0
  103. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/docker/surrealdb/docker-compose.yml +0 -0
  104. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/profile.example.yaml +0 -0
  105. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/Dockerfile/Dockerfile +0 -0
  106. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/Dockerfile/digitalkin-1.0.0.dev0.tar.gz +0 -0
  107. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/Dockerfile/entrypoint.sh +0 -0
  108. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/Dockerfile/generate_certificates.py +0 -0
  109. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/README.md +0 -0
  110. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/__init__.py +0 -0
  111. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/application.py +0 -0
  112. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/config.py +0 -0
  113. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/handlers/__init__.py +0 -0
  114. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/handlers/input_factory.py +0 -0
  115. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/handlers/output_handler.py +0 -0
  116. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/main.py +0 -0
  117. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/models/__init__.py +0 -0
  118. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/models/config.py +0 -0
  119. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/models/protocols.py +0 -0
  120. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/pyproject.toml +0 -0
  121. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/services/__init__.py +0 -0
  122. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/services/filesystem.py +0 -0
  123. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/services/gateway_client.py +0 -0
  124. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/services/module_client.py +0 -0
  125. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/services/registry.py +0 -0
  126. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/services/setup.py +0 -0
  127. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/chainlit/uv.lock +0 -0
  128. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/horizon/Dockerfile/Dockerfile +0 -0
  129. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/services/horizon/Dockerfile/nginx.conf +0 -0
  130. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/conftest.py +0 -0
  131. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_base.py +0 -0
  132. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_binaries.py +0 -0
  133. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_docker.py +0 -0
  134. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_docker_validators.py +0 -0
  135. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_git.py +0 -0
  136. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_git_repo_ops.py +0 -0
  137. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_git_validators.py +0 -0
  138. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_process.py +0 -0
  139. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_redaction.py +0 -0
  140. {magicbox_dk-0.0.2.dev1 → magicbox_dk-0.0.3}/tests/test_settings.py +0 -0
@@ -3,11 +3,11 @@
3
3
  # https://peps.python.org/pep-0440/
4
4
 
5
5
  [tool.bumpversion]
6
- current_version = "0.0.2.dev1"
6
+ current_version = "0.0.3"
7
7
 
8
8
  allow_dirty = false # Require clean working directory
9
- commit = false # Don't auto-commit (let user control git)
10
- tag = true
9
+ commit = true # Auto-commit the version bump
10
+ tag = true # Auto-create the git tag (v{new_version})
11
11
  sign_tags = false # Don't sign tags
12
12
 
13
13
  # major.minor.patch with an optional `.devN` pre-release.
@@ -118,7 +118,7 @@ Point d'entrée : `from cli.runner import docker, git` (singletons).
118
118
  schéma non autorisé). Primitives validées
119
119
  (`status_porcelain`, `head_branch`, `rev_list_counts`…, `clone`/`fetch`/`checkout`/`pull_ff`) **et** orchestration UI
120
120
  repliée depuis l'ancien `git_ops`
121
- (`sync_repo`/`pull_repo`/`repo_status` + `RepoStatus`/`report_repo_statuses`). Consommé par `dk prepare`/`update`/
121
+ (`sync_repo`/`pull_repo`/`repo_status` + `RepoStatus`/`report_repo_statuses`). Consommé par `dk prepare`/`pull`/
122
122
  `start`.
123
123
 
124
124
  ### `cli/core/` — runtime state
@@ -238,16 +238,16 @@ class DeleteCommand(BaseCommand):
238
238
  `cli` est **l'unique méthode publique** : elle construit l'instance depuis la CLI
239
239
  et déclenche `_dispatch()` (le cycle de vie partagé). `BaseCommand` fournit un
240
240
  **`cli` par défaut** (classmethod `cls()._dispatch()`) : une commande **sans
241
- argument** (`version`, `dashboard`, `settings`) ne redéfinit pas `cli`, elle
241
+ argument** (`dashboard`, `settings`) ne redéfinit pas `cli`, elle
242
242
  hérite du défaut (on passe `help=` au montage pour garder une description
243
- distincte : `app.command("version", help="…")(VersionCommand.cli)`). Toute
243
+ distincte : `app.command("dashboard", help="…")(DashboardCommand.cli)`). Toute
244
244
  commande avec arguments/options **redéfinit** `cli` en `@staticmethod` typée.
245
245
 
246
246
  - `cli/commands/_base.py` — `BaseCommand` (ABC). Cycle de vie via attributs de
247
247
  classe surchargeables : `requires_profile` (bootstrap vs bootstrap_no_profile),
248
248
  `require_existing`, `require_prepared`, `requires_init` (False ⇒ pas de
249
249
  `require_initialized`, ex. `init`/`dashboard`/`doctor`), `loads_context`
250
- (False ⇒ aucun contexte, ex. `version`). `_dispatch()` fait le préambule +
250
+ (False ⇒ aucun contexte, ex. `update`). `_dispatch()` fait le préambule +
251
251
  `run()`, et enveloppe `run()` d'une **frontière `CmdError`** (échec d'une
252
252
  commande externe `check=True` → panneau d'erreur, sortie déjà masquée).
253
253
  Helpers : `fail(msg, code=, cause=)`, `confirm(msg, yes=)`,
@@ -295,10 +295,12 @@ Verbs (with aliases) :
295
295
  | `status, stats, ps` | Container state, section by section. |
296
296
  | `logs [service]` | `docker compose logs`. |
297
297
  | `shell, exec <service>` | `docker exec -it` into a container of the active profile. |
298
- | `update [name...]` | `git pull --ff-only` on clones of the active profile. |
298
+ | `pull, sync [name...]` | `git pull --ff-only` on clones of the active profile. |
299
299
  | `validate [profile]` | Validate YAML (Pydantic), no side effects. |
300
300
  | `doctor` | Check Docker, Compose, git, user structure. |
301
- | `version` | Print Magicbox version. |
301
+ | `update [--check]` | Update the CLI itself (`magicbox-dk`) from PyPI. |
302
+
303
+ The root `dk --version` (`-V`) prints the current CLI version and exits (there is no `version` sub-command).
302
304
 
303
305
  The entrypoint `dk = "cli.main:app"` is declared in `pyproject.toml`.
304
306
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magicbox-dk
3
- Version: 0.0.2.dev1
3
+ Version: 0.0.3
4
4
  Summary: Magicbox for help DigitalKin developpers.
5
5
  Author-email: Alexandre TRESALLET <a.tresall@digitalkin.ai>
6
6
  License-Expression: MIT
@@ -41,6 +41,7 @@ stack — le tout stocké hors du repo, dans le dossier de config standard de vo
41
41
  - [Settings, globals & credentials](#settings-globals--credentials)
42
42
  - [Multi-profils](#multi-profils)
43
43
  - [Auth git](#auth-git)
44
+ - [Versioning & release](#versioning--release)
44
45
  - [Dépannage](#dépannage)
45
46
  - [License](#license)
46
47
 
@@ -304,6 +305,66 @@ Magicbox délègue l'authentification au système : configurez votre credential
304
305
 
305
306
  ---
306
307
 
308
+ ## Versioning & release
309
+
310
+ Le versionnage suit **PEP 440** : `major.minor.patch` avec un pré-release `.devN` optionnel,
311
+ piloté par [`bump-my-version`](https://github.com/callowayproject/bump-my-version) (config dans
312
+ `.bumpversion.toml`). Chaque `bump` met à jour `pyproject.toml` **et** `uv.lock`, puis **commit +
313
+ tag** automatiquement (`commit = true`, `tag = true`).
314
+
315
+ > Le binaire Homebrew de `bump-my-version` peut être cassé (interpréteur Python obsolète) —
316
+ > préfixe les commandes par `uv run` pour utiliser celui du venv.
317
+
318
+ **Règle d'or** : un `.devN` est toujours un pré-release de la **prochaine** version pas encore
319
+ sortie (`0.0.2.dev1 < 0.0.2`). La base `major.minor.patch` ne fait que **monter** — on ne colle
320
+ jamais un `.dev` sur une version déjà publiée.
321
+
322
+ ### Les cas
323
+
324
+ | Cas | Commande | Exemple |
325
+ |-----------------------------------------------|------------------------------------|-----------------------------|
326
+ | **Ouvrir un cycle dev** (depuis une finale) | `bump patch` \| `minor` \| `major` | `0.0.1` → `0.0.2.dev1` |
327
+ | **Itérer en dev** | `bump dev` | `0.0.2.dev1` → `0.0.2.dev2` |
328
+ | **Passer d'une dev à la release** (finaliser) | `bump --new-version X.Y.Z` | `0.0.2.dev2` → `0.0.2` |
329
+
330
+ > `bump dev` depuis une **finale** échoue (`value 0 is lower than the first value 1`) : c'est voulu
331
+ > (`first_value = 1`). Depuis une finale, on ouvre le cycle avec `patch`/`minor`/`major`, jamais `dev`.
332
+ > Et « finaliser » (retirer le `.dev`) se fait avec `--new-version`, car un compteur `.dev` ne peut
333
+ > pas redescendre à 0 tout seul.
334
+
335
+ ### Procédure complète (pour chaque version)
336
+
337
+ ```bash
338
+ # 1. bump : édite pyproject.toml + uv.lock, commit + tag automatiques
339
+ uv run bump-my-version bump patch # 0.0.1 → 0.0.2.dev1
340
+
341
+ # 2. pousse le commit ET son tag
342
+ git push --follow-tags origin main
343
+
344
+ # 3. crée la GitHub Release → déclenche la publication (.github/workflows/release.yml)
345
+ gh release create v0.0.2.dev1 --prerelease --generate-notes # --prerelease pour un .dev
346
+ ```
347
+
348
+ Exemple d'un cycle complet `0.0.1 → 0.0.2` :
349
+
350
+ ```bash
351
+ uv run bump-my-version bump patch # 0.0.1 → 0.0.2.dev1
352
+ git push --follow-tags origin main
353
+ gh release create v0.0.2.dev1 --prerelease --generate-notes
354
+
355
+ uv run bump-my-version bump dev # 0.0.2.dev1 → 0.0.2.dev2
356
+ git push --follow-tags origin main
357
+ gh release create v0.0.2.dev2 --prerelease --generate-notes
358
+
359
+ uv run bump-my-version bump --new-version 0.0.2 # finalise : 0.0.2.dev2 → 0.0.2
360
+ git push --follow-tags origin main
361
+ gh release create v0.0.2 --generate-notes # release finale (sans --prerelease)
362
+ ```
363
+
364
+ `bump-my-version show-bump` affiche à tout moment les transitions possibles depuis la version courante.
365
+
366
+ ---
367
+
307
368
  ## Dépannage
308
369
 
309
370
  | Symptôme | Remède |
@@ -24,6 +24,7 @@ stack — le tout stocké hors du repo, dans le dossier de config standard de vo
24
24
  - [Settings, globals & credentials](#settings-globals--credentials)
25
25
  - [Multi-profils](#multi-profils)
26
26
  - [Auth git](#auth-git)
27
+ - [Versioning & release](#versioning--release)
27
28
  - [Dépannage](#dépannage)
28
29
  - [License](#license)
29
30
 
@@ -287,6 +288,66 @@ Magicbox délègue l'authentification au système : configurez votre credential
287
288
 
288
289
  ---
289
290
 
291
+ ## Versioning & release
292
+
293
+ Le versionnage suit **PEP 440** : `major.minor.patch` avec un pré-release `.devN` optionnel,
294
+ piloté par [`bump-my-version`](https://github.com/callowayproject/bump-my-version) (config dans
295
+ `.bumpversion.toml`). Chaque `bump` met à jour `pyproject.toml` **et** `uv.lock`, puis **commit +
296
+ tag** automatiquement (`commit = true`, `tag = true`).
297
+
298
+ > Le binaire Homebrew de `bump-my-version` peut être cassé (interpréteur Python obsolète) —
299
+ > préfixe les commandes par `uv run` pour utiliser celui du venv.
300
+
301
+ **Règle d'or** : un `.devN` est toujours un pré-release de la **prochaine** version pas encore
302
+ sortie (`0.0.2.dev1 < 0.0.2`). La base `major.minor.patch` ne fait que **monter** — on ne colle
303
+ jamais un `.dev` sur une version déjà publiée.
304
+
305
+ ### Les cas
306
+
307
+ | Cas | Commande | Exemple |
308
+ |-----------------------------------------------|------------------------------------|-----------------------------|
309
+ | **Ouvrir un cycle dev** (depuis une finale) | `bump patch` \| `minor` \| `major` | `0.0.1` → `0.0.2.dev1` |
310
+ | **Itérer en dev** | `bump dev` | `0.0.2.dev1` → `0.0.2.dev2` |
311
+ | **Passer d'une dev à la release** (finaliser) | `bump --new-version X.Y.Z` | `0.0.2.dev2` → `0.0.2` |
312
+
313
+ > `bump dev` depuis une **finale** échoue (`value 0 is lower than the first value 1`) : c'est voulu
314
+ > (`first_value = 1`). Depuis une finale, on ouvre le cycle avec `patch`/`minor`/`major`, jamais `dev`.
315
+ > Et « finaliser » (retirer le `.dev`) se fait avec `--new-version`, car un compteur `.dev` ne peut
316
+ > pas redescendre à 0 tout seul.
317
+
318
+ ### Procédure complète (pour chaque version)
319
+
320
+ ```bash
321
+ # 1. bump : édite pyproject.toml + uv.lock, commit + tag automatiques
322
+ uv run bump-my-version bump patch # 0.0.1 → 0.0.2.dev1
323
+
324
+ # 2. pousse le commit ET son tag
325
+ git push --follow-tags origin main
326
+
327
+ # 3. crée la GitHub Release → déclenche la publication (.github/workflows/release.yml)
328
+ gh release create v0.0.2.dev1 --prerelease --generate-notes # --prerelease pour un .dev
329
+ ```
330
+
331
+ Exemple d'un cycle complet `0.0.1 → 0.0.2` :
332
+
333
+ ```bash
334
+ uv run bump-my-version bump patch # 0.0.1 → 0.0.2.dev1
335
+ git push --follow-tags origin main
336
+ gh release create v0.0.2.dev1 --prerelease --generate-notes
337
+
338
+ uv run bump-my-version bump dev # 0.0.2.dev1 → 0.0.2.dev2
339
+ git push --follow-tags origin main
340
+ gh release create v0.0.2.dev2 --prerelease --generate-notes
341
+
342
+ uv run bump-my-version bump --new-version 0.0.2 # finalise : 0.0.2.dev2 → 0.0.2
343
+ git push --follow-tags origin main
344
+ gh release create v0.0.2 --generate-notes # release finale (sans --prerelease)
345
+ ```
346
+
347
+ `bump-my-version show-bump` affiche à tout moment les transitions possibles depuis la version courante.
348
+
349
+ ---
350
+
290
351
  ## Dépannage
291
352
 
292
353
  | Symptôme | Remède |
@@ -21,6 +21,7 @@ est délégué à :class:`cli.ui.StepReporter` exposé via ``self.steps``.
21
21
  from __future__ import annotations
22
22
 
23
23
  import itertools
24
+ import sys
24
25
  import threading
25
26
  import time
26
27
  from abc import ABC, abstractmethod
@@ -32,6 +33,7 @@ from typing import Annotated, Any, NoReturn, TypeVar
32
33
  import questionary
33
34
  import typer
34
35
 
36
+ from cli import version_check
35
37
  from cli.core.cmd import bootstrap, bootstrap_no_profile, resolve_target
36
38
  from cli.core.context import MagicboxContext
37
39
  from cli.debug import dlog, set_debug, set_verbose
@@ -77,6 +79,10 @@ class BaseCommand(ABC):
77
79
  #: commandes « profil courant » (``start``/``edit``/``prepare``…) gardent
78
80
  #: ``False`` (défaut → profil actif, pas de prompt).
79
81
  picks_profile: bool = False
82
+ #: ``True`` ⇒ affiche (sur stderr, TTY uniquement) une ligne d'info si une
83
+ #: version plus récente est publiée sur PyPI. Désactivé sur ``version`` (qui
84
+ #: l'affiche à sa façon) et ``update`` (c'est déjà la mise à jour du CLI).
85
+ notifies_upgrade: bool = True
80
86
 
81
87
  def __init__(
82
88
  self, profile: str | None = None, *,
@@ -119,6 +125,13 @@ class BaseCommand(ABC):
119
125
  # Verbose déroule le détail des étapes (cf. ``-v/--verbose``) — posé avant
120
126
  # tout pour couvrir aussi le préambule. ``set_debug`` est géré par bootstrap.
121
127
  set_verbose(self.verbose)
128
+ # Vérif de version : uniquement en terminal interactif (jamais en
129
+ # script/CI/pipe). Le rafraîchissement du cache est lancé en tâche de
130
+ # fond ici (max de temps pour finir pendant `run`) ; la notice, lue au
131
+ # cache, est affichée après un `run` réussi.
132
+ notify_upgrade = self.notifies_upgrade and sys.stderr.isatty()
133
+ if notify_upgrade:
134
+ version_check.trigger_background_refresh()
122
135
  if self.requires_profile:
123
136
  self.ctx, self.profile_id = bootstrap(
124
137
  self.profile_name,
@@ -140,6 +153,10 @@ class BaseCommand(ABC):
140
153
 
141
154
  try:
142
155
  self.run()
156
+ if notify_upgrade:
157
+ notice = version_check.notice_if_outdated()
158
+ if notice:
159
+ typer.secho(notice, fg=typer.colors.YELLOW, err=True)
143
160
  except CmdError as exc:
144
161
  # Échec d'une commande externe en mode ``check=True`` (git/docker).
145
162
  # ``exc.output`` est déjà masqué des secrets par le runner.
@@ -14,9 +14,9 @@ import typer
14
14
  import yaml
15
15
 
16
16
  from cli.commands._base import BaseCommand
17
- from cli.commands.version import package_version
18
17
  from cli.core.context import MagicboxContext
19
18
  from cli.ui import GREY_FG, Report, short_home_path
19
+ from cli.version_check import current_version
20
20
 
21
21
  # Sous-commandes mises en avant dans le mémo de démarrage (verbe → résumé).
22
22
  QUICK_COMMANDS: tuple[tuple[str, str], ...] = (
@@ -42,7 +42,7 @@ class DashboardCommand(BaseCommand):
42
42
  # ── En-tête : titre + description ─────────────────────────────────── #
43
43
  r.blank()
44
44
  title = typer.style("Magicbox", fg=typer.colors.CYAN, bold=True)
45
- ver = typer.style(f"v{package_version()}", fg=GREY_FG)
45
+ ver = typer.style(f"v{current_version()}", fg=GREY_FG)
46
46
  typer.echo(f" {title} · {ver}")
47
47
  r.detail(
48
48
  "Orchestrateur d'environnements de dev pour les services & archetypes DigitalKin.",
@@ -46,7 +46,9 @@ from cli.compose import (
46
46
  generate_horizon_compose,
47
47
  generate_node_compose,
48
48
  generate_nsp_compose,
49
+ plan_build,
49
50
  report_compose_failure,
51
+ save_manifest,
50
52
  )
51
53
  from cli.compose.network import (
52
54
  AllocatedPorts,
@@ -272,6 +274,7 @@ class PrepareCommand(StepCommand):
272
274
  self.project = self.ctx.compose_project(self.profile_id)
273
275
  self.repos_dir = self.ctx.profile_repos_dir(self.profile_id)
274
276
  self.overrides_dir = self.ctx.profile_overrides_dir(self.profile_id)
277
+ self.manifest_path = self.ctx.profile_build_manifest(self.profile_id)
275
278
  self.env_dir = self.ctx.profile_env_dir(self.profile_id)
276
279
  self.network_path = self.ctx.profile_network_yaml(self.profile_id)
277
280
  self.settings_ports = self.ctx.settings.ports
@@ -290,6 +293,7 @@ class PrepareCommand(StepCommand):
290
293
  self.infra_vars: dict[str, str] = {}
291
294
  self.build_failed = False
292
295
  self.build_result: Any = None
296
+ self.build_plan: Any = None # cli.compose.BuildPlan | None (rempli au build)
293
297
 
294
298
  self.steps.section(f"Profil : {self.ctx.profile_name(self.profile_id)}")
295
299
 
@@ -816,11 +820,36 @@ class PrepareCommand(StepCommand):
816
820
  def prebuild_images(self) -> None:
817
821
  files, profiles = compose_args_for(self.profile, self.overrides_dir)
818
822
  build_env = compute_compose_env(self.ctx, self.profile, self.profile_id)
823
+ # Rebuild incrémental : on ne (re)construit que les images dont un input
824
+ # de build a changé (build args, HEAD du dépôt, Dockerfile, image
825
+ # absente, contexte dev dirty). Cf. `cli.compose.build_state`. Le calcul
826
+ # du plan lit `docker compose config` → sous le même env que le build.
827
+ with compose_env_context(build_env):
828
+ plan = plan_build(
829
+ self.project, files, profiles, self.manifest_path, force=self.force,
830
+ )
831
+ self.build_plan = plan
832
+
833
+ if plan.nothing_to_do:
834
+ # Toutes les images sont à jour : aucun `docker compose build` lancé.
835
+ self.steps.set_activity("images à jour — aucun rebuild")
836
+ return
837
+
838
+ if plan.degraded:
839
+ # Config compose illisible : repli sûr = tout reconstruire.
840
+ services: list[str] = []
841
+ else:
842
+ services = plan.to_build
843
+ self.steps.set_activity(
844
+ f"reconstruction de {len(services)} image(s) : {', '.join(services)}",
845
+ )
846
+
819
847
  # Le viewer affiche la progression BuildKit + un rappel de touche, et
820
848
  # permet d'expand/collapse la sortie Docker complète (touche ``o``/espace).
821
849
  with compose_env_context(build_env), _BuildOutputViewer(self.steps) as viewer:
822
850
  result = docker.compose_build(
823
851
  self.project, files, profiles,
852
+ services=services,
824
853
  no_cache=self.force,
825
854
  on_progress=viewer.feed_progress,
826
855
  on_output=viewer.feed_output,
@@ -831,6 +860,10 @@ class PrepareCommand(StepCommand):
831
860
  # l'échec et ``finish()`` affiche l'extrait borné hors panneau.
832
861
  self.build_failed = True
833
862
  self.build_result = result
863
+ elif not plan.degraded:
864
+ # Build réussi : on fige les signatures courantes (celles des services
865
+ # skippés matchent déjà, celles des reconstruits sont désormais à jour).
866
+ save_manifest(self.manifest_path, plan.signatures)
834
867
 
835
868
  # ── Panneau final ────────────────────────────────────────────────────── #
836
869
 
@@ -848,7 +881,25 @@ class PrepareCommand(StepCommand):
848
881
  "`dk start` re-tentera le build au démarrage",
849
882
  )
850
883
  else:
851
- self.steps.panel_ok(f"Profil « {label} » préparé", "Étape suivante : dk start")
884
+ detail = "\n".join(
885
+ d for d in ("Étape suivante : dk start", self._build_summary()) if d
886
+ )
887
+ self.steps.panel_ok(f"Profil « {label} » préparé", detail)
888
+
889
+ def _build_summary(self) -> str | None:
890
+ """Résumé du rebuild incrémental (images reconstruites vs à jour)."""
891
+ plan = self.build_plan
892
+ if plan is None or self.no_build:
893
+ return None
894
+ if plan.degraded:
895
+ return "Build : plan incrémental indisponible — toutes les images (re)construites."
896
+ if not plan.to_build:
897
+ return f"Build : {len(plan.skipped)} image(s) déjà à jour — aucun rebuild."
898
+ built = ", ".join(plan.to_build)
899
+ line = f"Build : {len(plan.to_build)} image(s) reconstruite(s) ({built})"
900
+ if plan.skipped:
901
+ line += f" · {len(plan.skipped)} à jour (skip)"
902
+ return line
852
903
 
853
904
  # ── Helpers (tout dans la classe) ────────────────────────────────────── #
854
905
 
@@ -1,4 +1,4 @@
1
- """dk update — git pull --ff-only sur les clones du profil actif."""
1
+ """dk pull — git pull --ff-only sur les clones du profil actif."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -15,7 +15,7 @@ from cli.models.profile import runnable_entries
15
15
  from cli.runner import git
16
16
 
17
17
 
18
- class UpdateCommand(StepCommand):
18
+ class PullCommand(StepCommand):
19
19
  """git pull --ff-only sur les clones du profil actif.
20
20
 
21
21
  Chaque clone est mis à jour comme une étape numérotée ``[i/N] <repo>`` sous
@@ -23,7 +23,7 @@ class UpdateCommand(StepCommand):
23
23
  est capturé en mode collapse, déroulé en verbose/non-TTY.
24
24
  """
25
25
 
26
- command_label = "Mise à jour en cours"
26
+ command_label = "Récupération des dépôts (git pull)"
27
27
 
28
28
  def __init__(
29
29
  self, names: list[str] | None = None, *,
@@ -93,4 +93,4 @@ class UpdateCommand(StepCommand):
93
93
  ] = False,
94
94
  ) -> None:
95
95
  """git pull --ff-only sur les clones du profil actif."""
96
- UpdateCommand(names, debug=debug, verbose=verbose)._dispatch()
96
+ PullCommand(names, debug=debug, verbose=verbose)._dispatch()
@@ -435,10 +435,11 @@ class StartCommand(StepCommand):
435
435
  la progression (lancement puis état par service) alimente le panneau Live
436
436
  de la commande (``set_activity`` / ``set_detail``).
437
437
  """
438
- # Budget santé = valeur LIVE des settings (``timeouts.health_wait_s``),
439
- # décompté **dès le lancement** des conteneurs (``started_at`` ici, avant
440
- # le ``compose up``) pas à partir du moment le front devient healthy.
441
- # Ainsi le décompte affiché en phase 2 est déjà « pris en cours de route ».
438
+ # Budget santé = valeur LIVE des settings (``timeouts.health_wait_s``).
439
+ # ``started_at`` (avant le ``compose up``) ancre le **plafond global**
440
+ # tant que le frontend n'est pas healthy. Le **décompte de skip** affiché
441
+ # en phase 2 est, lui, ré-ancré à l'instant le front devient healthy et
442
+ # part donc de ``health_wait_s`` plein (cf. ``wait_until_healthy``).
442
443
  started_at = time.monotonic()
443
444
  health_timeout = self.ctx.settings.timeouts.health_wait_s
444
445
  if is_debug():
@@ -0,0 +1,140 @@
1
+ """dk update — met à jour le package ``magicbox-dk`` depuis PyPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from importlib.metadata import distribution
10
+ from typing import Annotated
11
+
12
+ import typer
13
+
14
+ from cli.commands._base import BaseCommand
15
+ from cli.ui import spinner
16
+ from cli.version_check import PACKAGE, current_version, fetch_latest
17
+
18
+
19
+ def _is_editable_install() -> bool:
20
+ """Vrai si ``magicbox-dk`` est installé en mode éditable (``pip install -e``).
21
+
22
+ On lit ``direct_url.json`` (PEP 610) de la distribution : les installs
23
+ éditables portent ``dir_info.editable = true``. Sert à **refuser** l'upgrade
24
+ dans un checkout de dev — sinon un ``pip install --upgrade`` remplacerait le
25
+ lien éditable par la release PyPI et casserait l'environnement de travail.
26
+ """
27
+ try:
28
+ raw = distribution(PACKAGE).read_text("direct_url.json")
29
+ if not raw:
30
+ return False
31
+ return bool(json.loads(raw).get("dir_info", {}).get("editable"))
32
+ except (OSError, ValueError, ImportError):
33
+ return False
34
+
35
+
36
+ def _upgrade_argv() -> tuple[list[str], str]:
37
+ """``(argv, méthode)`` pour mettre à jour selon le mode d'installation.
38
+
39
+ Détecte pipx / uv-tool via le préfixe d'environnement (ces gestionnaires
40
+ isolent le CLI dans un venv dédié où ``pip install`` ne conviendrait pas) ;
41
+ sinon retombe sur ``python -m pip install --upgrade`` de l'interpréteur
42
+ courant (cas pip / venv classique).
43
+ """
44
+ prefix = sys.prefix.replace("\\", "/").lower()
45
+ pipx = shutil.which("pipx")
46
+ if "/pipx/" in prefix and pipx:
47
+ return [pipx, "upgrade", PACKAGE], "pipx"
48
+ uv = shutil.which("uv")
49
+ if "/uv/tools/" in prefix and uv:
50
+ return [uv, "tool", "upgrade", PACKAGE], "uv tool"
51
+ return [sys.executable, "-m", "pip", "install", "--upgrade", PACKAGE], "pip"
52
+
53
+
54
+ class UpdateCommand(BaseCommand):
55
+ """Met à jour Magicbox vers la dernière version publiée sur PyPI."""
56
+
57
+ requires_profile = False
58
+ requires_init = False
59
+ loads_context = False
60
+ notifies_upgrade = False # c'est déjà la commande de mise à jour
61
+
62
+ def __init__(self, *, check: bool = False, debug: bool = False) -> None:
63
+ super().__init__(debug=debug)
64
+ self.check = check
65
+
66
+ def run(self) -> None:
67
+ current = current_version()
68
+ self.steps.section(f"Magicbox {current}")
69
+
70
+ with spinner("Recherche d'une version plus récente sur PyPI…"):
71
+ latest = fetch_latest()
72
+ if latest is None:
73
+ self.steps.warn("Impossible de contacter PyPI — réessaie plus tard.")
74
+ return
75
+
76
+ if not self._is_newer(current, latest):
77
+ self.steps.ok(f"déjà à jour (dernière version : {latest})")
78
+ return
79
+
80
+ self.steps.info(f"version {latest} disponible (installée : {current})")
81
+ if self.check:
82
+ self.steps.info("lance `dk update` (sans `--check`) pour l'installer")
83
+ return
84
+
85
+ if _is_editable_install():
86
+ self.steps.warn(
87
+ "Installation éditable (dépôt de dev) détectée — upgrade désactivé "
88
+ "pour ne pas écraser ton checkout.",
89
+ "Mets à jour via `git pull` dans le dépôt.",
90
+ )
91
+ return
92
+
93
+ argv, method = _upgrade_argv()
94
+ self.steps.info(f"mise à jour via {method}…")
95
+ # Sortie de l'installeur héritée directement (progression pip/uv en direct).
96
+ result = subprocess.run(argv, check=False) # noqa: S603 (argv construit en interne)
97
+ if result.returncode == 0:
98
+ self.steps.panel_ok(
99
+ f"Magicbox mis à jour vers {latest}",
100
+ "relance une commande `dk` pour utiliser la nouvelle version",
101
+ )
102
+ else:
103
+ self.fail(
104
+ f"Échec de la mise à jour (code {result.returncode}). "
105
+ f"Essaie manuellement : {' '.join(argv)}",
106
+ )
107
+
108
+ @staticmethod
109
+ def _is_newer(current: str, latest: str) -> bool:
110
+ """Vrai si ``latest`` est strictement plus récent que ``current`` (PEP 440).
111
+
112
+ Sans ``packaging`` ou avec une version courante non standard (``dev`` /
113
+ install source), on renvoie ``True`` par défaut : l'utilisateur a lancé
114
+ ``dk update`` explicitement, on ne le bloque pas sur une comparaison
115
+ impossible (le garde-fou install-éditable protège déjà le cas dev).
116
+ """
117
+ if current == latest:
118
+ return False
119
+ try:
120
+ from packaging.version import InvalidVersion, Version
121
+ except ImportError:
122
+ return True
123
+ try:
124
+ return Version(latest) > Version(current)
125
+ except InvalidVersion:
126
+ return True
127
+
128
+ @staticmethod
129
+ def cli(
130
+ check: Annotated[
131
+ bool,
132
+ typer.Option("--check", help="Vérifie seulement (sans installer) s'il y a une mise à jour."),
133
+ ] = False,
134
+ debug: Annotated[
135
+ bool,
136
+ typer.Option("--debug", "-d", help="Logs détaillés."),
137
+ ] = False,
138
+ ) -> None:
139
+ """Met à jour Magicbox vers la dernière version publiée sur PyPI."""
140
+ UpdateCommand(check=check, debug=debug)._dispatch()
@@ -23,6 +23,12 @@ le port pré-alloué par ``dk prepare`` : ils ne consultent plus les endpoints.
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ # ── Rebuild incrémental des images ──────────────────────────────────────────── #
27
+ from cli.compose.build_state import (
28
+ BuildPlan,
29
+ plan_build,
30
+ save_manifest,
31
+ )
26
32
  # ── Builder (génération d'overrides) ────────────────────────────────────────── #
27
33
  from cli.compose.builder import (
28
34
  generate_back_compose,
@@ -63,6 +69,7 @@ from cli.compose.runtime import (
63
69
  )
64
70
 
65
71
  __all__ = [
72
+ "BuildPlan",
66
73
  "RESTART_LIMIT",
67
74
  "allocate_back_port",
68
75
  "allocate_back_worker_port",
@@ -88,8 +95,10 @@ __all__ = [
88
95
  "generate_nsp_compose",
89
96
  "magicbox_container_publishing",
90
97
  "magicbox_orphan_resources",
98
+ "plan_build",
91
99
  "project_built_images",
92
100
  "report_compose_failure",
93
101
  "running_compose_projects",
102
+ "save_manifest",
94
103
  "wait_until_healthy",
95
104
  ]