splent-cli 1.4.4__tar.gz → 1.5.0__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 (161) hide show
  1. {splent_cli-1.4.4/src/splent_cli.egg-info → splent_cli-1.5.0}/PKG-INFO +3 -2
  2. {splent_cli-1.4.4 → splent_cli-1.5.0}/pyproject.toml +4 -2
  3. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_upgrade.py +3 -1
  4. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_add.py +29 -29
  5. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_attach.py +22 -15
  6. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_detach.py +19 -22
  7. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_edit.py +59 -118
  8. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_release.py +6 -0
  9. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_remove.py +18 -32
  10. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature_compile.py +1 -1
  11. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_configure.py +6 -0
  12. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_create.py +64 -13
  13. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_derive.py +36 -41
  14. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_env.py +31 -46
  15. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_port.py +10 -8
  16. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_run.py +2 -3
  17. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_up.py +22 -12
  18. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_validate.py +11 -5
  19. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_configs.py +5 -2
  20. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/uvl/uvl_utils.py +17 -2
  21. splent_cli-1.5.0/src/splent_cli/services/preflight.py +70 -0
  22. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/feature_utils.py +89 -0
  23. {splent_cli-1.4.4 → splent_cli-1.5.0/src/splent_cli.egg-info}/PKG-INFO +3 -2
  24. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli.egg-info/requires.txt +3 -1
  25. splent_cli-1.4.4/src/splent_cli/services/preflight.py +0 -86
  26. {splent_cli-1.4.4 → splent_cli-1.5.0}/LICENSE +0 -0
  27. {splent_cli-1.4.4 → splent_cli-1.5.0}/README.md +0 -0
  28. {splent_cli-1.4.4 → splent_cli-1.5.0}/setup.cfg +0 -0
  29. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/__init__.py +0 -0
  30. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/__main__.py +0 -0
  31. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/cli.py +0 -0
  32. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/__init__.py +0 -0
  33. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  34. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  35. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  36. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  37. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  38. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  39. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  40. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  41. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  42. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/__init__.py +0 -0
  43. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  44. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  45. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_env.py +0 -0
  46. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_features.py +0 -0
  47. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_github.py +0 -0
  48. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  49. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_product.py +0 -0
  50. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  51. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  52. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/clear_cache.py +0 -0
  53. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/clear_log.py +0 -0
  54. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/clear_uploads.py +0 -0
  55. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/command_create.py +0 -0
  56. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/coverage.py +0 -0
  57. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_console.py +0 -0
  58. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  59. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  60. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  61. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  62. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  63. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  64. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/database/db_status.py +0 -0
  65. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/doctor.py +0 -0
  66. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/env/env_list.py +0 -0
  67. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/env/env_set.py +0 -0
  68. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/env/env_show.py +0 -0
  69. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/export_puml.py +0 -0
  70. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  71. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  72. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  73. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  74. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_diff.py +0 -0
  75. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  76. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  77. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  78. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  79. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  80. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  81. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  82. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  83. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  84. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  85. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  86. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  87. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  88. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  89. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  90. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  91. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  92. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  93. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  94. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  95. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  96. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  97. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  98. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  99. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/linter.py +0 -0
  100. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/locust.py +0 -0
  101. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/__init__.py +0 -0
  102. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_build.py +0 -0
  103. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  104. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  105. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_complete.py +0 -0
  106. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_config.py +0 -0
  107. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_console.py +0 -0
  108. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  109. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  110. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_down.py +0 -0
  111. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  112. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_list.py +0 -0
  113. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  114. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  115. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_release.py +0 -0
  116. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  117. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  118. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_select.py +0 -0
  119. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  120. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  121. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_status.py +0 -0
  122. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_sync.py +0 -0
  123. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  124. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/product/product_test.py +0 -0
  125. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/release/__init__.py +0 -0
  126. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/release/release_core.py +0 -0
  127. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/selenium.py +0 -0
  128. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  129. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  130. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  131. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  132. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  133. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  134. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_fix.py +0 -0
  135. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  136. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  137. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  138. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/tokens.py +0 -0
  139. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/commands/version.py +0 -0
  140. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/services/__init__.py +0 -0
  141. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/services/compose.py +0 -0
  142. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/services/context.py +0 -0
  143. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/services/release.py +0 -0
  144. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/__init__.py +0 -0
  145. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/cache_utils.py +0 -0
  146. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/command_loader.py +0 -0
  147. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  148. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/db_utils.py +0 -0
  149. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/decorators.py +0 -0
  150. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  151. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/feature_installer.py +0 -0
  152. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/git_url.py +0 -0
  153. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/integrity.py +0 -0
  154. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/lifecycle.py +0 -0
  155. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/manifest.py +0 -0
  156. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/path_utils.py +0 -0
  157. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli/utils/template_drift.py +0 -0
  158. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli.egg-info/SOURCES.txt +0 -0
  159. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  160. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  161. {splent_cli-1.4.4 → splent_cli-1.5.0}/src/splent_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_cli
3
- Version: 1.4.4
3
+ Version: 1.5.0
4
4
  Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
5
5
  Author-email: DiversoLab <diversolab@us.es>
6
6
  Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
@@ -21,7 +21,6 @@ Requires-Dist: black==24.10.0; extra == "dev"
21
21
  Requires-Dist: coverage==7.6.10; extra == "dev"
22
22
  Requires-Dist: docker==7.1.0; extra == "dev"
23
23
  Requires-Dist: Faker==33.3.1; extra == "dev"
24
- Requires-Dist: flamapy==2.5.0; extra == "dev"
25
24
  Requires-Dist: flake8==7.1.1; extra == "dev"
26
25
  Requires-Dist: graphviz==0.20.3; extra == "dev"
27
26
  Requires-Dist: iniconfig==2.0.0; extra == "dev"
@@ -37,6 +36,8 @@ Requires-Dist: selenium-wire==5.1.0; extra == "dev"
37
36
  Requires-Dist: pip-tools==7.4.1; extra == "dev"
38
37
  Requires-Dist: tomli_w==1.2.0; extra == "dev"
39
38
  Requires-Dist: PyYAML==6.0.3; extra == "dev"
39
+ Provides-Extra: uvl
40
+ Requires-Dist: flamapy==2.5.0; extra == "uvl"
40
41
  Dynamic: license-file
41
42
 
42
43
  # SPLENT CLI
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.4.4"
7
+ version = "1.5.0"
8
8
  description = "SPLENT-CLI is a CLI to be able to work on your development more easily."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -28,7 +28,6 @@ dev = [
28
28
  "coverage==7.6.10",
29
29
  "docker==7.1.0",
30
30
  "Faker==33.3.1",
31
- "flamapy==2.5.0",
32
31
  "flake8==7.1.1",
33
32
  "graphviz==0.20.3",
34
33
  "iniconfig==2.0.0",
@@ -45,6 +44,9 @@ dev = [
45
44
  "tomli_w==1.2.0",
46
45
  "PyYAML==6.0.3"
47
46
  ]
47
+ uvl = [
48
+ "flamapy==2.5.0",
49
+ ]
48
50
 
49
51
  [project.scripts]
50
52
  splent = "splent_cli.__main__:main"
@@ -59,15 +59,17 @@ def db_upgrade(feature):
59
59
  import logging
60
60
 
61
61
  logging.getLogger("alembic").setLevel(logging.WARNING)
62
+ logging.getLogger("alembic.runtime.migration").setLevel(logging.WARNING)
62
63
 
63
64
  for feat, mdir in dirs.items():
64
65
  try:
66
+ logging.getLogger("alembic.runtime.migration").setLevel(logging.WARNING)
65
67
  alembic_upgrade(directory=mdir)
66
68
  revision = MigrationManager.get_current_feature_revision(
67
69
  feat, app.extensions["migrate"].db.engine
68
70
  )
69
71
  MigrationManager.update_feature_status(app, feat, revision)
70
- click.echo(click.style(f"{feat} {revision or 'head'}", fg="green"))
72
+ click.echo(click.style(f" {feat} -> {revision or 'head'}", fg="green"))
71
73
 
72
74
  # Advance lifecycle state to "migrated"
73
75
  info = entry_lookup.get(feat)
@@ -3,7 +3,7 @@ import tomllib
3
3
  import tomli_w
4
4
  import click
5
5
  from splent_cli.services import context
6
- from splent_cli.utils.feature_utils import normalize_namespace
6
+ from splent_cli.utils.feature_utils import normalize_namespace, hot_reinstall
7
7
  from splent_cli.utils.manifest import feature_key, set_feature_state
8
8
 
9
9
 
@@ -39,11 +39,8 @@ def feature_add(full_name, env_scope):
39
39
  splent feature:add splent-io/splent_feature_admin --dev
40
40
  """
41
41
 
42
- # --------------------------
43
- # 0️⃣ Validate format
44
- # --------------------------
45
42
  if "/" not in full_name:
46
- click.echo("Invalid format. Use: <namespace>/<feature_name>")
43
+ click.secho(" Invalid format. Use: <namespace>/<feature_name>", fg="red")
47
44
  raise SystemExit(1)
48
45
 
49
46
  namespace, feature_name = full_name.split("/", 1)
@@ -55,16 +52,17 @@ def feature_add(full_name, env_scope):
55
52
  # Editable features live at workspace root
56
53
  feature_dir = os.path.join(workspace, feature_name)
57
54
  if not os.path.exists(feature_dir):
58
- click.echo(f" Feature not found at workspace root: {feature_dir}")
59
- click.echo(f" Create it first with: splent feature:create {full_name}")
55
+ click.secho(f" {feature_name} not found at workspace root.", fg="red")
56
+ click.echo(
57
+ click.style(" create it first: ", dim=True)
58
+ + f"splent feature:create {full_name}"
59
+ )
60
60
  raise SystemExit(1)
61
61
 
62
- # --------------------------
63
- # 1️⃣ Update pyproject.toml
64
- # --------------------------
62
+ # ── Update pyproject.toml ─────────────────────────────────────────
65
63
  pyproject_path = os.path.join(workspace, product, "pyproject.toml")
66
64
  if not os.path.exists(pyproject_path):
67
- click.echo("pyproject.toml not found in product directory.")
65
+ click.secho(" pyproject.toml not found.", fg="red")
68
66
  raise SystemExit(1)
69
67
 
70
68
  with open(pyproject_path, "rb") as f:
@@ -82,19 +80,21 @@ def feature_add(full_name, env_scope):
82
80
  else (data.get("tool", {}).get("splent", {}).get(features_key, []))
83
81
  )
84
82
 
85
- if full_name not in features:
86
- features.append(full_name)
87
- write_features_to_data(data, features, key=features_key)
88
- with open(pyproject_path, "wb") as f:
89
- tomli_w.dump(data, f)
90
- scope_label = f" ({env_scope} only)" if env_scope else ""
91
- click.echo(f"🧩 Added '{full_name}' to {features_key}{scope_label}.")
92
- else:
93
- click.echo(f"ℹ️ Feature '{full_name}' already present in {features_key}.")
94
-
95
- # --------------------------
96
- # 2️⃣ Create symlink
97
- # --------------------------
83
+ short = feature_name.replace("splent_feature_", "")
84
+
85
+ if full_name in features:
86
+ click.echo(f" {short} already in {features_key}.")
87
+ return
88
+
89
+ features.append(full_name)
90
+ write_features_to_data(data, features, key=features_key)
91
+ with open(pyproject_path, "wb") as f:
92
+ tomli_w.dump(data, f)
93
+
94
+ scope_label = f" ({env_scope} only)" if env_scope else ""
95
+ click.echo(f" {short} added to {features_key}{scope_label}")
96
+
97
+ # ── Create symlink ────────────────────────────────────────────────
98
98
  product_features_dir = os.path.join(workspace, product, "features", org_safe)
99
99
  os.makedirs(product_features_dir, exist_ok=True)
100
100
 
@@ -105,11 +105,8 @@ def feature_add(full_name, env_scope):
105
105
  except FileExistsError:
106
106
  os.unlink(link_path)
107
107
  os.symlink(rel_target, link_path)
108
- click.echo(f"🔗 Linked {link_path} → {rel_target}")
109
108
 
110
- # --------------------------
111
- # 3️⃣ Update manifest
112
- # --------------------------
109
+ # ── Update manifest ───────────────────────────────────────────────
113
110
  product_path = os.path.join(workspace, product)
114
111
  key = feature_key(namespace, feature_name)
115
112
  set_feature_state(
@@ -123,4 +120,7 @@ def feature_add(full_name, env_scope):
123
120
  mode="editable",
124
121
  )
125
122
 
126
- click.echo(f"✅ Feature '{full_name}' added successfully to product '{product}'.")
123
+ # ── Hot reinstall in web container ────────────────────────────────
124
+ hot_reinstall(product_path, f"/workspace/{feature_name}", feature_name)
125
+
126
+ click.secho(" done.", fg="green")
@@ -3,6 +3,7 @@ import tomllib
3
3
  import tomli_w
4
4
  import click
5
5
  from splent_cli.services import context, compose
6
+ from splent_cli.utils.feature_utils import hot_reinstall
6
7
  from splent_cli.utils.manifest import feature_key, set_feature_state
7
8
 
8
9
 
@@ -32,7 +33,7 @@ def feature_attach(feature_identifier, version, env_scope):
32
33
  If not, run: splent feature:clone <namespace>/<feature>@<version>
33
34
  - Updates pyproject.toml referencing feature@version.
34
35
  - Creates/updates the versioned symlink in features/<namespace>/.
35
- - Updates the manifest state to 'declared'.
36
+ - Reinstalls the feature in the web container for hot reload.
36
37
  """
37
38
  product = context.require_app()
38
39
  ws = context.workspace()
@@ -47,22 +48,23 @@ def feature_attach(feature_identifier, version, env_scope):
47
48
  pyproject_path = os.path.join(product_path, "pyproject.toml")
48
49
 
49
50
  if not os.path.exists(pyproject_path):
50
- click.echo("pyproject.toml not found in product.")
51
+ click.secho(" pyproject.toml not found.", fg="red")
51
52
  raise SystemExit(1)
52
53
 
53
- # --- 1️⃣ Verify feature exists in cache ---------------------------------
54
+ # ── Verify feature exists in cache ────────────────────────────────
54
55
  versioned_dir = os.path.join(cache_base, f"{feature_name}@{version}")
55
56
 
56
57
  if not os.path.exists(versioned_dir):
58
+ click.secho(f" {feature_name}@{version} not found in cache.", fg="red")
57
59
  click.echo(
58
- f" Feature '{namespace}/{feature_name}@{version}' not found in cache.\n"
59
- f" Run first: splent feature:clone {namespace}/{feature_name}@{version}"
60
+ click.style(" clone it first: ", dim=True)
61
+ + f"splent feature:clone {namespace}/{feature_name}@{version}"
60
62
  )
61
63
  raise SystemExit(1)
62
64
 
63
- click.echo(f" Cache found → {versioned_dir}")
65
+ short = feature_name.replace("splent_feature_", "")
64
66
 
65
- # --- 2️⃣ Update pyproject.toml ------------------------------------------
67
+ # ── Update pyproject.toml ─────────────────────────────────────────
66
68
  full_name = f"{namespace}/{feature_name}@{version}"
67
69
  bare_name = f"{namespace}/{feature_name}"
68
70
 
@@ -82,9 +84,9 @@ def feature_attach(feature_identifier, version, env_scope):
82
84
  )
83
85
 
84
86
  if full_name in features:
85
- click.echo(f"ℹ️ Feature '{full_name}' already present in {features_key}.")
87
+ click.echo(f" {short}@{version} already in {features_key}.")
86
88
  else:
87
- # Replace bare entry (added by uvl:sync) or old versioned entry if present
89
+ # Replace bare entry or old versioned entry if present
88
90
  features = [
89
91
  f for f in features if f != bare_name and not f.startswith(f"{bare_name}@")
90
92
  ]
@@ -93,9 +95,9 @@ def feature_attach(feature_identifier, version, env_scope):
93
95
  with open(pyproject_path, "wb") as f:
94
96
  tomli_w.dump(data, f)
95
97
  scope_label = f" ({env_scope} only)" if env_scope else ""
96
- click.echo(f"🧩 Updated {features_key}{full_name}{scope_label}")
98
+ click.echo(f" {short}@{version} attached{scope_label}")
97
99
 
98
- # --- 3️⃣ Create/update symlink ------------------------------------------
100
+ # ── Create/update symlink ─────────────────────────────────────────
99
101
  product_features_dir = os.path.join(product_path, "features", namespace_fs)
100
102
  os.makedirs(product_features_dir, exist_ok=True)
101
103
 
@@ -105,9 +107,7 @@ def feature_attach(feature_identifier, version, env_scope):
105
107
  rel_target = os.path.relpath(versioned_dir, product_features_dir)
106
108
  os.symlink(rel_target, new_link)
107
109
 
108
- click.echo(f"🔗 Linked {new_link} {rel_target}")
109
-
110
- # --- 4️⃣ Update manifest ------------------------------------------------
110
+ # ── Update manifest ───────────────────────────────────────────────
111
111
  key = feature_key(namespace_fs, feature_name, version)
112
112
  set_feature_state(
113
113
  product_path,
@@ -120,4 +120,11 @@ def feature_attach(feature_identifier, version, env_scope):
120
120
  mode="pinned",
121
121
  )
122
122
 
123
- click.echo("🎯 Feature successfully attached.")
123
+ # ── Hot reinstall in web container ────────────────────────────────
124
+ # Symlink resolves to cache path — install from there
125
+ install_path = (
126
+ f"/workspace/{product}/features/{namespace_fs}/{feature_name}@{version}"
127
+ )
128
+ hot_reinstall(product_path, install_path, feature_name)
129
+
130
+ click.secho(" done.", fg="green")
@@ -2,6 +2,7 @@ import os
2
2
  import re
3
3
  import click
4
4
  from splent_cli.services import context, compose
5
+ from splent_cli.utils.feature_utils import hot_uninstall
5
6
  from splent_cli.utils.manifest import (
6
7
  feature_key,
7
8
  remove_feature,
@@ -39,39 +40,37 @@ def feature_detach(feature_identifier, version, force):
39
40
 
40
41
  product_path = str(ws / product)
41
42
  pyproject_path = os.path.join(product_path, "pyproject.toml")
43
+ short = feature_name.replace("splent_feature_", "")
42
44
 
43
45
  if not os.path.exists(pyproject_path):
44
- click.echo("pyproject.toml not found in product.")
46
+ click.secho(" pyproject.toml not found.", fg="red")
45
47
  raise SystemExit(1)
46
48
 
47
49
  if not force:
48
- # --- Guard: dependency check ----------------------------------------
50
+ # Guard: dependency check
49
51
  dependents = get_dependents(product_path, feature_name)
50
52
  if dependents:
51
53
  click.secho(
52
- f"Cannot detach '{feature_name}': the following installed features depend on it:\n"
53
- + "".join(f" {d}\n" for d in dependents)
54
- + " Remove those features first, or use --force to bypass.",
54
+ f" Cannot detach '{short}': the following features depend on it:\n"
55
+ + "".join(f" - {d}\n" for d in dependents)
56
+ + " Remove those first, or use --force.",
55
57
  fg="red",
56
58
  )
57
59
  raise SystemExit(1)
58
60
 
59
- # --- Guard: migration state -----------------------------------------
61
+ # Guard: migration state
60
62
  key = feature_key(namespace_fs, feature_name, version)
61
63
  state = get_feature_state(product_path, key)
62
64
  if state in ("migrated", "active"):
63
65
  click.secho(
64
- f"❌ Feature '{feature_name}' has migrations applied (state: {state}).\n"
65
- f" Roll them back first:\n"
66
- f" splent db:rollback {feature_name} --steps 999\n"
67
- f" Or use --force to skip this check.",
66
+ f" {short} has migrations applied (state: {state}).\n"
67
+ f" Roll them back first: splent db:rollback {feature_name} --steps 999\n"
68
+ f" Or use --force.",
68
69
  fg="red",
69
70
  )
70
71
  raise SystemExit(1)
71
72
 
72
- # --- 1️⃣ Remove versioned reference from pyproject ----------------------
73
- click.echo(f"🧹 Removing {feature_name}@{version} from pyproject.toml...")
74
-
73
+ # ── Remove versioned reference from pyproject ─────────────────────
75
74
  with open(pyproject_path, "r", encoding="utf-8") as f:
76
75
  content = f.read()
77
76
 
@@ -81,22 +80,20 @@ def feature_detach(feature_identifier, version, force):
81
80
  with open(pyproject_path, "w", encoding="utf-8") as f:
82
81
  f.write(new_content)
83
82
 
84
- click.echo("🧩 pyproject.toml cleaned.")
83
+ click.echo(f" {short}@{version} removed from pyproject.toml")
85
84
 
86
- # --- 2️⃣ Remove symlink --------------------------------------------------
85
+ # ── Remove symlink ────────────────────────────────────────────────
87
86
  product_features_dir = os.path.join(product_path, "features", namespace_fs)
88
87
  link_path = os.path.join(product_features_dir, f"{feature_name}@{version}")
89
88
 
90
89
  if os.path.islink(link_path):
91
90
  os.unlink(link_path)
92
- click.echo(f"🔗 Removed symlink: {link_path}")
93
- else:
94
- click.echo(
95
- f"⚠️ No symlink found for {feature_name}@{version} in {namespace_fs}/"
96
- )
97
91
 
98
- # --- 3️⃣ Update manifest ------------------------------------------------
92
+ # ── Update manifest ───────────────────────────────────────────────
99
93
  key = feature_key(namespace_fs, feature_name, version)
100
94
  remove_feature(str(ws / product), product, key)
101
95
 
102
- click.echo("🎯 Feature successfully detached.")
96
+ # ── Hot uninstall from web container ──────────────────────────────
97
+ hot_uninstall(product_path, feature_name)
98
+
99
+ click.secho(" done.", fg="green")
@@ -8,62 +8,6 @@ from splent_cli.utils.feature_utils import read_features_from_data
8
8
  from splent_cli.utils.cache_utils import make_feature_writable
9
9
 
10
10
 
11
- # =====================================================================
12
- # GITHUB WRITE ACCESS CHECK
13
- # =====================================================================
14
- def _has_write_access(ns_git: str, name: str) -> tuple[bool, str]:
15
- """Check if the authenticated user has push access to ns_git/name.
16
-
17
- Returns (has_access, reason_if_denied).
18
- """
19
- token = os.getenv("GITHUB_TOKEN")
20
- github_user = os.getenv("GITHUB_USER", "(not set)")
21
- if not token:
22
- return False, (
23
- f"GitHub user (env): {github_user}\n"
24
- f" Repo owner: {ns_git}\n"
25
- f" GITHUB_TOKEN not set"
26
- )
27
-
28
- headers = {
29
- "Accept": "application/vnd.github+json",
30
- "Authorization": f"token {token}",
31
- "User-Agent": "splent-cli",
32
- }
33
- try:
34
- resp = requests.get(
35
- f"https://api.github.com/repos/{ns_git}/{name}",
36
- headers=headers,
37
- timeout=5,
38
- )
39
- if resp.status_code == 404:
40
- return False, (
41
- f"GitHub user (env): {github_user}\n"
42
- f" Repo owner: {ns_git}\n"
43
- f" Repo {ns_git}/{name} not found (404)"
44
- )
45
- if resp.status_code == 200:
46
- perms = resp.json().get("permissions", {})
47
- if perms.get("push", False):
48
- return True, ""
49
- return False, (
50
- f"GitHub user (env): {github_user}\n"
51
- f" Repo owner: {ns_git}\n"
52
- f" Push access: {perms.get('push', False)}"
53
- )
54
- return False, (
55
- f"GitHub user (env): {github_user}\n"
56
- f" Repo owner: {ns_git}\n"
57
- f" GitHub API returned {resp.status_code}"
58
- )
59
- except requests.RequestException as e:
60
- return False, (
61
- f"GitHub user (env): {github_user}\n"
62
- f" Repo owner: {ns_git}\n"
63
- f" API error: {e}"
64
- )
65
-
66
-
67
11
  # =====================================================================
68
12
  # CACHE PATH RESOLVER
69
13
  # =====================================================================
@@ -140,39 +84,12 @@ def replace_pyproject_reference(pyproject_path: str, name: str, version: str):
140
84
 
141
85
 
142
86
  # =====================================================================
143
- # HOT REINSTALL — pip install + Flask reload in the web container
87
+ # HOT REINSTALL — delegated to shared utility
144
88
  # =====================================================================
145
89
  def _hot_reinstall(workspace: str, product_path: str, editable_path: str, name: str):
146
- """Reinstall the feature via pip in the product's web container and trigger Flask reload."""
147
- product = os.path.basename(product_path)
148
- env = os.getenv("SPLENT_ENV", "dev")
149
- docker_dir = os.path.join(product_path, "docker")
90
+ from splent_cli.utils.feature_utils import hot_reinstall
150
91
 
151
- compose_file = compose.resolve_file(product_path, env)
152
- if not compose_file:
153
- return # no docker-compose file — nothing to do
154
-
155
- pname = compose.project_name(product, env)
156
- container_id = compose.find_main_container(pname, compose_file, docker_dir)
157
- if not container_id:
158
- return # container not running — nothing to do
159
-
160
- # 1. pip install -e from the new path
161
- click.echo(f" 🔄 Reinstalling {name} in web container...")
162
- pip_cmd = (
163
- f"pip install --no-cache-dir --root-user-action=ignore -q -e /workspace/{name}"
164
- )
165
- subprocess.run(
166
- ["docker", "exec", container_id, "bash", "-c", pip_cmd],
167
- capture_output=True,
168
- )
169
-
170
- # 2. Touch the app's __init__.py to trigger watchmedo auto-restart
171
- init_py = f"/workspace/{product}/src/{product}/__init__.py"
172
- subprocess.run(
173
- ["docker", "exec", container_id, "bash", "-c", f"touch {init_py}"],
174
- capture_output=True,
175
- )
92
+ hot_reinstall(product_path, f"/workspace/{name}", name)
176
93
 
177
94
 
178
95
  # =====================================================================
@@ -205,7 +122,7 @@ def _compile_assets(workspace: str, product_path: str, name: str):
205
122
  if not container_id:
206
123
  return
207
124
 
208
- click.echo(" 📦 Compiling assets...")
125
+ click.echo(click.style(" compiling assets...", dim=True))
209
126
  product_root = f"/workspace/{product}"
210
127
  cmd = f"cd {shlex.quote(product_root)} && npx webpack --config {shlex.quote(webpack_file)} --mode development"
211
128
  result = subprocess.run(
@@ -213,10 +130,40 @@ def _compile_assets(workspace: str, product_path: str, name: str):
213
130
  capture_output=True,
214
131
  text=True,
215
132
  )
216
- if result.returncode == 0:
217
- click.echo(" ✔ Assets compiled.")
218
- else:
219
- click.secho(" ⚠ Asset compilation failed.", fg="yellow")
133
+ if result.returncode != 0:
134
+ click.secho(" asset compilation failed.", fg="yellow")
135
+
136
+
137
+ # =====================================================================
138
+ # WRITE ACCESS WARNING
139
+ # =====================================================================
140
+ def _warn_no_push_access(ns_git: str, name: str):
141
+ """Print a warning if the user cannot push to the repo. Non-blocking."""
142
+ token = os.getenv("GITHUB_TOKEN")
143
+ if not token:
144
+ return # no token = can't check, don't nag
145
+
146
+ headers = {
147
+ "Accept": "application/vnd.github+json",
148
+ "Authorization": f"token {token}",
149
+ "User-Agent": "splent-cli",
150
+ }
151
+ try:
152
+ resp = requests.get(
153
+ f"https://api.github.com/repos/{ns_git}/{name}",
154
+ headers=headers,
155
+ timeout=5,
156
+ )
157
+ if resp.status_code == 200:
158
+ perms = resp.json().get("permissions", {})
159
+ if not perms.get("push", False):
160
+ click.secho(
161
+ f" note: no push access to {ns_git}/{name}\n"
162
+ f" use 'splent feature:fork' to work on your own copy",
163
+ fg="yellow",
164
+ )
165
+ except requests.RequestException:
166
+ pass # network error — don't block
220
167
 
221
168
 
222
169
  # =====================================================================
@@ -239,38 +186,26 @@ def _edit_one(
239
186
  name, version = rest, None
240
187
 
241
188
  if not version:
242
- click.echo(f" ℹ️ {match} already editable, skipping.")
189
+ click.echo(click.style(f" {name}", dim=True) + " already editable")
243
190
  return True
244
191
 
245
- # Guard: require write access to the GitHub repo
246
- if not force:
247
- has_access, reason = _has_write_access(ns_git, name)
248
- if not has_access:
249
- click.secho(
250
- f" ❌ No write access to {ns_git}/{name}.\n"
251
- f" {reason}\n"
252
- f" To work on your own copy, use: splent feature:fork {ns_git}/{name}\n"
253
- f" Or use --force to bypass this check.",
254
- fg="red",
255
- )
256
- return False
257
-
258
- click.echo(f" 🧩 {ns_git}/{name}@{version}")
192
+ short = name.replace("splent_feature_", "")
193
+ click.echo(f" {short} ({version}) -> editable")
259
194
 
260
195
  versioned_path, editable_path = get_feature_paths(workspace, ns_fs, name, version)
261
196
 
262
197
  if not os.path.exists(versioned_path):
263
- click.secho(f" Versioned cache not found: {versioned_path}", fg="red")
198
+ click.secho(f" cached version not found: {versioned_path}", fg="red")
264
199
  return False
265
200
 
266
201
  if not os.path.exists(editable_path):
267
202
  import shutil
268
203
 
269
- click.echo(f" 📦 Creating editable copy → {editable_path}")
204
+ click.echo(click.style(" copying to workspace root...", dim=True))
270
205
  result = subprocess.run(["cp", "-r", versioned_path, editable_path])
271
206
  if result.returncode != 0:
272
207
  shutil.rmtree(editable_path, ignore_errors=True)
273
- click.secho(" Failed to copy feature to editable path.", fg="red")
208
+ click.secho(" failed to copy feature.", fg="red")
274
209
  return False
275
210
 
276
211
  # Always ensure editable copy is writable (cache files are read-only)
@@ -302,7 +237,10 @@ def _edit_one(
302
237
  # Compile webpack assets via the product's web container
303
238
  _compile_assets(workspace, product_path, name)
304
239
 
305
- click.secho(" ✔ ready for editing.", fg="green")
240
+ # Non-blocking warning if user can't push
241
+ _warn_no_push_access(ns_git, name)
242
+
243
+ click.secho(" ready.", fg="green")
306
244
  return True
307
245
 
308
246
 
@@ -329,7 +267,7 @@ def feature_edit(feature_name, edit_all, force):
329
267
  pyproject_path = os.path.join(product_path, "pyproject.toml")
330
268
 
331
269
  if not os.path.exists(pyproject_path):
332
- click.echo("pyproject.toml not found.")
270
+ click.secho(" pyproject.toml not found.", fg="red")
333
271
  raise SystemExit(1)
334
272
 
335
273
  if not feature_name and not edit_all:
@@ -353,7 +291,7 @@ def feature_edit(feature_name, edit_all, force):
353
291
  None,
354
292
  )
355
293
  if not match:
356
- click.echo(f"❌ Feature {feature_name} not found in pyproject.")
294
+ click.secho(f" {feature_name} not found in pyproject.", fg="red")
357
295
  raise SystemExit(1)
358
296
 
359
297
  click.echo()
@@ -367,17 +305,20 @@ def feature_edit(feature_name, edit_all, force):
367
305
 
368
306
  click.echo()
369
307
  if already_editable:
370
- click.secho(
371
- f" ℹ️ {len(already_editable)} feature(s) already editable — skipping.",
372
- fg="bright_black",
308
+ click.echo(
309
+ click.style(
310
+ f" {len(already_editable)} feature(s) already editable, skipping.",
311
+ dim=True,
312
+ )
373
313
  )
374
314
 
375
315
  if not versioned:
376
- click.secho(" All features are already editable.", fg="green")
316
+ click.secho(" All features are already editable.", fg="green")
377
317
  click.echo()
378
318
  return
379
319
 
380
- click.secho(f" Converting {len(versioned)} feature(s) to editable:\n", fg="cyan")
320
+ click.echo(f" Converting {len(versioned)} feature(s) to editable:")
321
+ click.echo()
381
322
  ok = 0
382
323
  for match in versioned:
383
324
  if _edit_one(workspace, product_path, pyproject_path, match, force=force):
@@ -385,7 +326,7 @@ def feature_edit(feature_name, edit_all, force):
385
326
  click.echo()
386
327
 
387
328
  click.secho(
388
- f" {ok}/{len(versioned)} feature(s) converted.",
329
+ f" {ok}/{len(versioned)} converted.",
389
330
  fg="green" if ok == len(versioned) else "yellow",
390
331
  )
391
332
  click.echo()
@@ -405,6 +405,12 @@ def feature_release(feature_ref, version, attach):
405
405
  post_pypi_hook=_post_pypi,
406
406
  )
407
407
 
408
+ if not attach:
409
+ product = context.require_app()
410
+ attach = click.confirm(
411
+ f"\n Attach {feature_name}@{tag} to {product}?", default=True
412
+ )
413
+
408
414
  if attach:
409
415
  click.echo(" attach linking to product...")
410
416
  ctx = click.get_current_context()