splent-cli 1.49.0__tar.gz → 1.50.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 (324) hide show
  1. {splent_cli-1.49.0/src/splent_cli.egg-info → splent_cli-1.50.0}/PKG-INFO +2 -2
  2. {splent_cli-1.49.0 → splent_cli-1.50.0}/pyproject.toml +2 -21
  3. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_infra.py +1 -1
  4. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/clear/clear_uploads.py +14 -1
  5. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_compat.py +1 -1
  6. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_compile.py +28 -0
  7. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_install.py +1 -1
  8. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_release.py +52 -16
  9. splent_cli-1.50.0/src/splent_cli/commands/feature/feature_security.py +114 -0
  10. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_test.py +1 -1
  11. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_unlock.py +12 -2
  12. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_xray.py +1 -1
  13. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_build.py +1 -14
  14. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_config.py +1 -1
  15. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_env.py +6 -0
  16. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_restart.py +43 -14
  17. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_routes.py +1 -1
  18. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_test.py +1 -1
  19. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_up.py +6 -0
  20. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_validate.py +112 -11
  21. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_configurations.py +20 -13
  22. splent_cli-1.50.0/src/splent_cli/commands/storage/storage_migrate.py +42 -0
  23. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/release_dry_run.py +21 -0
  24. splent_cli-1.50.0/src/splent_cli/services/security_scan.py +1109 -0
  25. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/spl_store.py +0 -4
  26. splent_cli-1.50.0/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +61 -0
  27. splent_cli-1.50.0/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +61 -0
  28. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +3 -0
  29. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +3 -2
  30. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +8 -0
  31. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +4 -1
  32. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_400.html +0 -2
  33. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_401.html +0 -2
  34. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_404.html +0 -2
  35. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_500.html +0 -2
  36. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_base_template.html +17 -5
  37. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +8 -3
  38. splent_cli-1.50.0/src/splent_cli/templates/product/product_pyproject.toml.j2 +69 -0
  39. splent_cli-1.50.0/src/splent_cli/utils/__init__.py +0 -0
  40. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/dynamic_imports.py +69 -1
  41. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/feature_utils.py +10 -5
  42. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/frontend_check.py +17 -3
  43. {splent_cli-1.49.0 → splent_cli-1.50.0/src/splent_cli.egg-info}/PKG-INFO +2 -2
  44. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli.egg-info/SOURCES.txt +4 -0
  45. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli.egg-info/requires.txt +1 -1
  46. splent_cli-1.49.0/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -100
  47. splent_cli-1.49.0/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -86
  48. splent_cli-1.49.0/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -93
  49. {splent_cli-1.49.0 → splent_cli-1.50.0}/COPYING +0 -0
  50. {splent_cli-1.49.0 → splent_cli-1.50.0}/COPYING.LESSER +0 -0
  51. {splent_cli-1.49.0 → splent_cli-1.50.0}/README.md +0 -0
  52. {splent_cli-1.49.0 → splent_cli-1.50.0}/setup.cfg +0 -0
  53. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/__init__.py +0 -0
  54. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/__main__.py +0 -0
  55. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/cli.py +0 -0
  56. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/__init__.py +0 -0
  57. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  58. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  59. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  60. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  61. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  62. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  63. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  64. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  65. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  66. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/__init__.py +0 -0
  67. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  68. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  69. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  70. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_env.py +0 -0
  71. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_features.py +0 -0
  72. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_github.py +0 -0
  73. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_product.py +0 -0
  74. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  75. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  76. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  77. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  78. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  79. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  80. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  81. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/command/__init__.py +0 -0
  82. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/command/command_create.py +0 -0
  83. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/coverage.py +0 -0
  84. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_console.py +0 -0
  85. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  86. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  87. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  88. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  89. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  90. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  91. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_status.py +0 -0
  92. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  93. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/doctor.py +0 -0
  94. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/env/env_list.py +0 -0
  95. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/env/env_set.py +0 -0
  96. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/env/env_show.py +0 -0
  97. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/export/__init__.py +0 -0
  98. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  99. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  100. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  101. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  102. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  103. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  104. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  105. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  106. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  107. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  108. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  109. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  110. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  111. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_frontend_init.py +0 -0
  112. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  113. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  114. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  115. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  116. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  117. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  118. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  119. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  120. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_npm.py +0 -0
  121. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  122. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  123. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  124. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  125. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  126. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  127. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  128. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  129. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  130. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  131. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  132. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  133. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  134. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  135. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/lint.py +0 -0
  136. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/locust.py +0 -0
  137. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  138. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  139. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  140. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  141. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  142. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/__init__.py +0 -0
  143. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  144. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  145. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  146. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  147. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_console.py +0 -0
  148. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  149. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_create.py +0 -0
  150. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  151. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  152. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  153. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_down.py +0 -0
  154. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  155. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_frontend_audit.py +0 -0
  156. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_list.py +0 -0
  157. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  158. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  159. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_params.py +0 -0
  160. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_port.py +0 -0
  161. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_release.py +0 -0
  162. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  163. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_run.py +0 -0
  164. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_select.py +0 -0
  165. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  166. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  167. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  168. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/release/__init__.py +0 -0
  169. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/release/release_core.py +0 -0
  170. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  171. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/selenium.py +0 -0
  172. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  173. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  174. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  175. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  176. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  177. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  178. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  179. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  180. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  181. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  182. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  183. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  184. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  185. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  186. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  187. {splent_cli-1.49.0/src/splent_cli/services → splent_cli-1.50.0/src/splent_cli/commands/storage}/__init__.py +0 -0
  188. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  189. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  190. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/commands/version.py +0 -0
  191. {splent_cli-1.49.0/src/splent_cli/utils → splent_cli-1.50.0/src/splent_cli/services}/__init__.py +0 -0
  192. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/compose.py +0 -0
  193. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/context.py +0 -0
  194. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/credentials.py +0 -0
  195. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  196. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/editable_refresh.py +0 -0
  197. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/frontend.py +0 -0
  198. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/image_registry.py +0 -0
  199. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/index_refresh.py +0 -0
  200. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/marketplace.py +0 -0
  201. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/marketplace_api.py +0 -0
  202. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/marketplace_url.py +0 -0
  203. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/neutral_app.py +0 -0
  204. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/own_commands.py +0 -0
  205. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/preflight.py +0 -0
  206. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/pypi_quota.py +0 -0
  207. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/registry.py +0 -0
  208. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/release.py +0 -0
  209. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/release_gate.py +0 -0
  210. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/services/web_container.py +0 -0
  211. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  212. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  213. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  214. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  215. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  216. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  217. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  218. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  219. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  220. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_frontend.npmrc.j2 +0 -0
  221. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_frontend_scripts.js.j2 +0 -0
  222. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  223. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  224. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  225. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  226. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  227. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  228. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  229. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  230. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  231. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  232. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  233. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  234. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  235. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  236. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_services_service.py.j2 +0 -0
  237. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  238. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  239. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  240. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  241. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  242. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  243. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  244. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  245. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  246. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  247. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  248. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  249. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  250. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  251. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  252. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  253. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  254. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  255. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  256. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  257. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  258. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  259. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  260. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  261. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  262. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_app.css +0 -0
  263. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_app.js +0 -0
  264. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  265. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
  266. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  267. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  268. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  269. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  270. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  271. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  272. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  273. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  274. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  275. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_own.css +0 -0
  276. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  277. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  278. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/Dockerfile.frontend +0 -0
  279. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/package-lock.json +0 -0
  280. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/package.json +0 -0
  281. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/splent/build.js +0 -0
  282. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/splent/legacy.js +0 -0
  283. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/splent/lint.js +0 -0
  284. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/splent/plugin.js +0 -0
  285. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/splent/preset.js +0 -0
  286. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/toolchain/frontend/splent/singletons.json +0 -0
  287. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/archetype.py +0 -0
  288. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/cache_utils.py +0 -0
  289. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/ci_services.py +0 -0
  290. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  291. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/command_loader.py +0 -0
  292. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  293. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/db_client.py +0 -0
  294. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/db_utils.py +0 -0
  295. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/decorators.py +0 -0
  296. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/deploy_freshness.py +0 -0
  297. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/feature_installer.py +0 -0
  298. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/feature_naming.py +0 -0
  299. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/feature_params.py +0 -0
  300. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  301. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  302. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/forge.py +0 -0
  303. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/frontend_licenses.py +0 -0
  304. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/git_url.py +0 -0
  305. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/integrity.py +0 -0
  306. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/io_utils.py +0 -0
  307. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/lifecycle.py +0 -0
  308. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/manifest.py +0 -0
  309. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/menu_keys.py +0 -0
  310. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/migration_lock.py +0 -0
  311. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/npm_semver.py +0 -0
  312. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/path_utils.py +0 -0
  313. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/proc.py +0 -0
  314. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/redact.py +0 -0
  315. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  316. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/running_version.py +0 -0
  317. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/template_drift.py +0 -0
  318. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/toml_edit.py +0 -0
  319. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  320. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/uvl_diagnosis.py +0 -0
  321. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli/utils/uvl_params.py +0 -0
  322. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  323. {splent_cli-1.49.0 → splent_cli-1.50.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  324. {splent_cli-1.49.0 → splent_cli-1.50.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.49.0
3
+ Version: 1.50.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
  License-Expression: LGPL-3.0-or-later
@@ -16,7 +16,7 @@ Requires-Dist: packaging>=24.0
16
16
  Requires-Dist: python-dotenv>=1.0
17
17
  Requires-Dist: pyyaml>=6.0
18
18
  Requires-Dist: requests>=2.31.0
19
- Requires-Dist: splent_framework<2,>=1.22.0
19
+ Requires-Dist: splent_framework<2,>=1.30.0
20
20
  Requires-Dist: tomli_w>=1.0.0
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: setuptools==80.3.1; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.49.0"
7
+ version = "1.50.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"
@@ -12,10 +12,6 @@ authors = [{ name = "DiversoLab", email = "diversolab@us.es" }]
12
12
  license = "LGPL-3.0-or-later"
13
13
  license-files = ["COPYING", "COPYING.LESSER"]
14
14
 
15
- # Everything the CLI imports at module level must be listed here (or come
16
- # with splent_framework, see tests/unit/test_declared_dependencies.py):
17
- # startup imports every command module, so one undeclared import breaks
18
- # `splent --help` on a clean `pip install splent_cli`.
19
15
  dependencies = [
20
16
  "click==8.1.8",
21
17
  "jinja2>=3.1",
@@ -23,15 +19,7 @@ dependencies = [
23
19
  "python-dotenv>=1.0",
24
20
  "pyyaml>=6.0",
25
21
  "requests>=2.31.0",
26
- # The CLI drives the framework (paths, feature order, migrations, the
27
- # app factory) and only works with the same major version of it. 1.22.0
28
- # brings splent_framework.params, the [tool.splent.config_schema] reader
29
- # product:validate, product:env and product:build share with the boot;
30
- # 1.18.0 resolves a feature to the copy feature:test names
31
- # (SPLENT_FEATURE_SOURCES) and brings the production defaults the deploy
32
- # templates explain; 1.17.0 the refinement rules product:validate shares
33
- # with the boot.
34
- "splent_framework>=1.22.0,<2",
22
+ "splent_framework>=1.30.0,<2",
35
23
  "tomli_w>=1.0.0",
36
24
  ]
37
25
 
@@ -76,10 +64,6 @@ package-dir = { "" = "src" }
76
64
  [tool.setuptools.packages.find]
77
65
  where = ["src"]
78
66
 
79
- # The templates product:create, feature:create and the rest render. They are
80
- # data, not modules, so without this a wheel from PyPI shipped none of them.
81
- # The build context of the frontend toolchain image too, so a CLI installed
82
- # from PyPI can build the image a feature with frontend/ builds in.
83
67
  [tool.setuptools.package-data]
84
68
  splent_cli = ["templates/**/*", "toolchain/**/*"]
85
69
 
@@ -87,9 +71,6 @@ splent_cli = ["templates/**/*", "toolchain/**/*"]
87
71
  testpaths = ["tests"]
88
72
  pythonpath = ["src"]
89
73
 
90
- # Canonical formatter/linter is Ruff (`splent lint` runs `ruff check` +
91
- # `ruff format`). Ruff's default line length (88) is the project standard;
92
- # set explicitly here so it is unambiguous.
93
74
  [tool.ruff]
94
75
  line-length = 88
95
76
 
@@ -366,7 +366,7 @@ def check_infra():
366
366
 
367
367
  data = load_toml(pyproject_path, what="pyproject.toml")
368
368
 
369
- env = os.getenv("SPLENT_ENV", "dev")
369
+ env = context.resolve_env()
370
370
  features = read_features_from_data(data, env)
371
371
 
372
372
  # Every compose file is read through the product's merged .env, the same one
@@ -32,6 +32,13 @@ _SYSTEM_DIRS = {
32
32
  "/tmp",
33
33
  "/usr",
34
34
  "/var",
35
+ # macOS: its own top level, and /private, where /etc, /tmp and /var lead.
36
+ "/Applications",
37
+ "/Library",
38
+ "/System",
39
+ "/Users",
40
+ "/Volumes",
41
+ "/private",
35
42
  }
36
43
 
37
44
  # What a directory of code has at its top and a directory of uploads never
@@ -77,6 +84,12 @@ def _configured_dir(app) -> str:
77
84
  return os.getenv("PROTECTED_UPLOADS_DIR", "").strip()
78
85
 
79
86
 
87
+ def _system_dirs() -> set:
88
+ """The system directories, and where each leads: on macOS /etc is a link
89
+ to /private/etc, which the realpath of a configured /etc gives."""
90
+ return _SYSTEM_DIRS | {os.path.realpath(path) for path in _SYSTEM_DIRS}
91
+
92
+
80
93
  def _refuse(path: str, real: str, why: str):
81
94
  shown = path if path == real else f"{path} (resolves to {real})"
82
95
  raise UploadsDirError(f"{shown} {why}. Nothing was deleted.")
@@ -124,7 +137,7 @@ def resolve_uploads_dir(app=None) -> str:
124
137
  )
125
138
 
126
139
  real = os.path.realpath(uploads_dir)
127
- if real == os.sep or real in _SYSTEM_DIRS:
140
+ if real == os.sep or real in _system_dirs():
128
141
  _refuse(uploads_dir, real, "is a system directory")
129
142
  if real == os.path.realpath(os.path.expanduser("~")):
130
143
  _refuse(uploads_dir, real, "is the home directory")
@@ -735,7 +735,7 @@ def run_all_product_check(
735
735
  if not labeled_paths:
736
736
  return []
737
737
  if env is None:
738
- env = os.getenv("SPLENT_ENV") or "dev"
738
+ env = context.resolve_env()
739
739
  labeled_contracts = [(lbl, _read_contract(path)) for lbl, path in labeled_paths]
740
740
  findings = _analyse_all(labeled_contracts) + version_floor_findings(
741
741
  labeled_paths
@@ -1,4 +1,5 @@
1
1
  import dataclasses
2
+ import hashlib
2
3
  import logging
3
4
  import os
4
5
  import posixpath
@@ -553,6 +554,31 @@ def _find_webpack(workspace, product, org_safe, base_name, version):
553
554
  return None
554
555
 
555
556
 
557
+ def _one_watcher(webpack_file: str, product_root: str, shell_cmd: str) -> str:
558
+ """The shell line that watches ``webpack_file`` and stops its watcher
559
+ before, where webpack runs.
560
+
561
+ Each start of a product in development (product:run --dev) compiles
562
+ with --watch again, and a watcher used to outlive the one that started
563
+ it: eight starts left eight generations of npm, sh and webpack, 2.4 GB
564
+ of a 7.8 GB Docker VM. The watcher now runs in a session of its own
565
+ (setsid), whose id it leaves in a file named after its bundle, and the
566
+ next start ends that whole group first, if it is still a webpack one.
567
+ """
568
+ folder = "/tmp/splent-watch-$(id -u)"
569
+ name = hashlib.sha1(webpack_file.encode()).hexdigest()[:16]
570
+ pidfile = f"{folder}/{name}.pid"
571
+ inner = f'echo $$ > "{pidfile}"; exec {shell_cmd}'
572
+ return (
573
+ f'mkdir -p "{folder}"; '
574
+ f'if [ -f "{pidfile}" ]; then '
575
+ f'old="$(cat "{pidfile}")"; '
576
+ f'if grep -qa webpack "/proc/$old/cmdline" 2>/dev/null; then '
577
+ f'kill -TERM -- "-$old" 2>/dev/null || true; fi; fi; '
578
+ f"cd {shlex.quote(product_root)} && exec setsid bash -c {shlex.quote(inner)}"
579
+ )
580
+
581
+
556
582
  def _compile_in_container(
557
583
  container_id,
558
584
  feature,
@@ -645,6 +671,8 @@ def _compile_in_container(
645
671
 
646
672
  shell_cmd = " ".join(shlex.quote(p) for p in cmd_parts)
647
673
  cd_cmd = f"cd {shlex.quote(product_root)} && {shell_cmd}"
674
+ if watch and not production:
675
+ cd_cmd = _one_watcher(webpack_file, product_root, shell_cmd)
648
676
 
649
677
  # Inside a container: run webpack directly.
650
678
  # From the host: run via `docker exec` so webpack uses the container's
@@ -230,7 +230,7 @@ def feature_install(feature_identifier, env_scope, mode, version):
230
230
  """
231
231
  product = context.require_app()
232
232
  workspace = str(context.workspace())
233
- env_name = os.getenv("SPLENT_ENV", "dev")
233
+ env_name = context.resolve_env()
234
234
 
235
235
  # ── Parse identifier ──────────────────────────────────────────────
236
236
  namespace, namespace_github, namespace_fs, feature_name = (
@@ -1505,8 +1505,14 @@ def _local_strings(function, names: dict[str, str]) -> dict[str, str]:
1505
1505
  return found
1506
1506
 
1507
1507
 
1508
+ #: The framework's lookups of a module whose feature the product may not have
1509
+ #: (splent_framework.optional): what they name is always an optional feature.
1510
+ _OPTIONAL_LOOKUPS = ("optional_module", "optional_attr")
1511
+
1512
+
1508
1513
  def _import_module_calls(tree, constants: dict[str, str]):
1509
1514
  """``(call, names)`` for every ``importlib.import_module`` call of TREE,
1515
+ and every call of the framework's ``optional_module``/``optional_attr``,
1510
1516
  NAMES being what its module may be spelled through: the module's string
1511
1517
  constants, and the names the function around the call binds."""
1512
1518
 
@@ -1515,9 +1521,9 @@ def _import_module_calls(tree, constants: dict[str, str]):
1515
1521
  if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
1516
1522
  yield from visit(child, {**names, **_local_strings(child, names)})
1517
1523
  continue
1518
- if isinstance(child, ast.Call) and _call_name(child) in (
1519
- "import_module",
1520
- "importlib.import_module",
1524
+ if isinstance(child, ast.Call) and (
1525
+ _call_name(child) in ("import_module", "importlib.import_module")
1526
+ or _call_name(child).rsplit(".", 1)[-1] in _OPTIONAL_LOOKUPS
1521
1527
  ):
1522
1528
  yield child, names
1523
1529
  yield from visit(child, names)
@@ -1583,8 +1589,9 @@ def _presence_guards(
1583
1589
  def _extract_dynamic_imports(
1584
1590
  src_dir: Path, own_feature_name: str
1585
1591
  ) -> tuple[set[str], set[str]]:
1586
- """Other features imported by name with ``importlib.import_module``,
1587
- split into ``(hard, soft)`` short names.
1592
+ """Other features imported by name with ``importlib.import_module``, or
1593
+ looked up with the framework's ``optional_module``/``optional_attr``
1594
+ (always soft), split into ``(hard, soft)`` short names.
1588
1595
 
1589
1596
  A feature that wants another one only when the product has it imports
1590
1597
  it by name inside a ``try`` whose handler catches ``ImportError`` (or
@@ -1610,9 +1617,12 @@ def _extract_dynamic_imports(
1610
1617
  module = _spelled_module(node.args[0], names) if node.args else None
1611
1618
  if not module:
1612
1619
  continue
1620
+ looked_up = _call_name(node).rsplit(".", 1)[-1] in _OPTIONAL_LOOKUPS
1613
1621
  for short in _named_features(module, own_feature_name):
1614
- optional = _in_ranges(node.lineno, ranges) or _in_ranges(
1615
- node.lineno, present.get(short, ())
1622
+ optional = (
1623
+ looked_up
1624
+ or _in_ranges(node.lineno, ranges)
1625
+ or _in_ranges(node.lineno, present.get(short, ()))
1616
1626
  )
1617
1627
  (soft if optional else hard).add(short)
1618
1628
  return hard, soft - hard
@@ -1890,6 +1900,11 @@ def infer_contract(
1890
1900
  docker = _extract_docker(feature_root)
1891
1901
  docker_contract = _extract_docker_contract(feature_root)
1892
1902
  req_features, env_vars = _scan_dependencies(src_dir, feature_name)
1903
+ # A key declared in [tool.splent.config_schema] is read from the
1904
+ # environment by the framework on the feature's behalf, with no
1905
+ # os.getenv left in its code to find: it is as much an environment
1906
+ # variable of the feature as one it reads itself.
1907
+ env_vars = sorted(set(env_vars) | declared_param_keys(feature_root))
1893
1908
  # And the settings it reads from app.config, which are as much its
1894
1909
  # parameters as what it reads from the environment, but are not
1895
1910
  # environment variables: check:product requires every env_var, and a
@@ -2088,8 +2103,8 @@ def _contract_comments(region: str) -> dict:
2088
2103
  ``{(table, key): (lines above, inline comment)}``, where ``key`` is None
2089
2104
  for the comments above a subtable's header. The contract is written
2090
2105
  again from the source, so the comments go with what they are about: the
2091
- key under or after them in the same table. The banner above
2092
- ``[tool.splent.contract]`` is the writer's own and is written anyway.
2106
+ key under or after them in the same table. The banner earlier writers
2107
+ put above ``[tool.splent.contract]`` is not kept.
2093
2108
  """
2094
2109
  comments: dict = {}
2095
2110
  table = None
@@ -2333,13 +2348,6 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
2333
2348
 
2334
2349
  contract_block = (
2335
2350
  "\n\n"
2336
- "# ── Feature Contract (auto-generated) ────────────────────────────────────────\n"
2337
- "# Run `splent feature:contract --write` to refresh it from the source.\n"
2338
- "# A refresh keeps description, category, tags, env,\n"
2339
- "# requires.features_manual (dependencies invisible to static analysis),\n"
2340
- "# requires.features_optional (what the scan infers is added to it)\n"
2341
- "# and provides.routes_manual (routes built from configuration at\n"
2342
- "# registration time, which have no literal path to find in source).\n"
2343
2351
  "[tool.splent.contract]\n"
2344
2352
  f"description = {_toml_value(existing_description)}\n"
2345
2353
  + (
@@ -2954,6 +2962,9 @@ def feature_release(
2954
2962
  _refuse_invalid_requirements(feature_path, namespace, feature_name)
2955
2963
  # A migration no database can record, refused the same way.
2956
2964
  _refuse_long_revision_ids(feature_path, feature_name)
2965
+ # SQL or markup built from a variable string, and a template that runs
2966
+ # inline code, refused the same way.
2967
+ _refuse_security_findings(feature_path, feature_name)
2957
2968
 
2958
2969
  release.confirm_single_channel(channels, no_forge=no_forge, no_pypi=no_pypi)
2959
2970
 
@@ -3209,6 +3220,31 @@ def _refuse_long_revision_ids(feature_path, feature_name) -> None:
3209
3220
  raise SystemExit(1)
3210
3221
 
3211
3222
 
3223
+ def _refuse_security_findings(feature_path, feature_name) -> None:
3224
+ """Stop the release, before it changes anything, when the feature's
3225
+ sources build SQL or markup from a variable string, render a template
3226
+ from one, or have a template that escapes nothing or runs inline code
3227
+ (splent_cli.services.security_scan). --skip-checks does not skip it.
3228
+ """
3229
+ from splent_cli.services import security_scan
3230
+
3231
+ findings = security_scan.scan_feature(feature_path)
3232
+ if not findings:
3233
+ return
3234
+ click.echo()
3235
+ click.secho(
3236
+ f" error: {feature_name} has {len(findings)} SQL or XSS sink(s) the "
3237
+ "toolchain refuses, so the release did not start.",
3238
+ fg="red",
3239
+ )
3240
+ security_scan.echo_findings(findings)
3241
+ click.echo(
3242
+ f" {security_scan.ALLOW_HINT}\n"
3243
+ " Nothing was changed. No version bump, no commit, no tag, no release."
3244
+ )
3245
+ raise SystemExit(1)
3246
+
3247
+
3212
3248
  def _interactive() -> bool:
3213
3249
  """Whether someone can answer a prompt on stdin."""
3214
3250
  import sys
@@ -0,0 +1,114 @@
1
+ """
2
+ feature:security, the SQL and XSS sinks of one feature or of every one.
3
+
4
+ The scan is splent_cli.services.security_scan, the one product:validate
5
+ runs in its Security phase and feature:release runs before it changes
6
+ anything.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+ from splent_cli.services import context, security_scan
14
+ from splent_cli.utils.feature_resolution import resolve_feature_ref
15
+ from splent_cli.utils.feature_utils import editable_feature_dirs
16
+
17
+
18
+ def _targets(workspace: Path, identifiers) -> list[tuple[str, Path]]:
19
+ """``(name, root)`` of each feature to scan: the ones named, else every
20
+ editable feature at the workspace root."""
21
+ if not identifiers:
22
+ return [
23
+ (leaf, Path(path)) for _, leaf, path in editable_feature_dirs(workspace)
24
+ ]
25
+ targets = []
26
+ for identifier in identifiers:
27
+ found = resolve_feature_ref(identifier, workspace)
28
+ if found is None:
29
+ click.secho(
30
+ f" Feature '{identifier}' not found at the workspace root or "
31
+ "in .splent_cache.",
32
+ fg="red",
33
+ )
34
+ raise SystemExit(1)
35
+ root, _, name = found
36
+ targets.append((root.name if "@" in root.name else name, root))
37
+ return targets
38
+
39
+
40
+ @click.command(
41
+ "feature:security",
42
+ short_help="Find the SQL and XSS sinks the toolchain refuses in features.",
43
+ )
44
+ @click.argument("feature_identifiers", nargs=-1)
45
+ def feature_security(feature_identifiers):
46
+ """
47
+ Scan features for SQL injection and XSS sinks no runtime can catch.
48
+
49
+ \b
50
+ Python
51
+ sql-from-string SQL of text(), execute(), exec_driver_sql(),
52
+ literal_column(), or a string given to filter(),
53
+ where(), order_by() or having(), built from a
54
+ variable string (an f-string, %, + or format())
55
+ markup-from-string Markup() of a string built that way from values
56
+ that are not escaped
57
+ template-from-string render_template_string() of a template that is
58
+ not a literal
59
+ Templates (.html, .j2, .xml, .svg under templates/)
60
+ autoescape-off {% autoescape false %}
61
+ inline-handler an on...= attribute
62
+ inline-script a <script> with code, no src and no nonce
63
+ javascript-url href, src, action or formaction with javascript:
64
+
65
+ \b
66
+ A reviewed exception says so on its line.
67
+ # splent: allow <rule> in Python
68
+ {# splent: allow <rule> #} in a template
69
+
70
+ \b
71
+ FEATURE_IDENTIFIERS are short names (notes), package names
72
+ (splent_feature_notes), either under a namespace (splent-io/notes),
73
+ or snapshots (notes@v1.2.0). Without any, every editable feature at
74
+ the workspace root is scanned. Tests are not scanned. Exits 1 when
75
+ there is a finding.
76
+
77
+ \b
78
+ Examples:
79
+ splent feature:security notes
80
+ splent feature:security auth profile
81
+ splent feature:security
82
+ """
83
+ workspace = Path(context.workspace())
84
+ targets = _targets(workspace, feature_identifiers)
85
+
86
+ click.echo()
87
+ click.echo(click.style(" Security scan (SQL and XSS sinks)", bold=True))
88
+ click.echo()
89
+
90
+ total = 0
91
+ flagged = 0
92
+ for name, root in targets:
93
+ findings = security_scan.scan_feature(root)
94
+ if not findings:
95
+ continue
96
+ flagged += 1
97
+ total += len(findings)
98
+ click.echo(click.style(f" {name}", bold=True))
99
+ security_scan.echo_findings(findings)
100
+ click.echo()
101
+
102
+ scanned = f"{len(targets)} feature" + ("" if len(targets) == 1 else "s")
103
+ if total:
104
+ click.secho(
105
+ f" ❌ {total} finding(s) in {flagged} of {scanned}.", fg="red", bold=True
106
+ )
107
+ click.echo(f" {security_scan.ALLOW_HINT}")
108
+ click.echo()
109
+ raise SystemExit(1)
110
+ click.secho(f" ✅ No findings in {scanned}.", fg="green", bold=True)
111
+ click.echo()
112
+
113
+
114
+ cli_command = feature_security
@@ -175,7 +175,7 @@ def _passthrough_args(
175
175
 
176
176
  def _is_prod(value) -> bool:
177
177
  """SPLENT_ENV read the way production_active() reads it."""
178
- return (value or "").strip().lower() == "prod"
178
+ return context.normalize_env(value) == "prod"
179
179
 
180
180
 
181
181
  def _is_prod_active(product: str | None = None) -> bool:
@@ -203,6 +203,11 @@ def replace_pyproject_reference(pyproject_path: str, name: str, version: str):
203
203
 
204
204
  if not changed:
205
205
  return
206
+ # Back to the editable checkout, which is never installed from PyPI, so
207
+ # the PyPI name the pinned release had is forgotten, as detach does.
208
+ from splent_cli.utils.feature_utils import set_feature_distribution
209
+
210
+ set_feature_distribution(data, name, None)
206
211
 
207
212
  write_toml(pyproject_path, data)
208
213
 
@@ -230,7 +235,7 @@ def _compile_assets(workspace: str, product_path: str, name: str):
230
235
  from splent_cli.services import frontend
231
236
 
232
237
  product = os.path.basename(product_path)
233
- env = os.getenv("SPLENT_ENV", "dev")
238
+ env = context.resolve_env()
234
239
 
235
240
  # Find webpack config
236
241
  editable_path = os.path.join(workspace, name)
@@ -457,7 +462,12 @@ def _edit_one(
457
462
  is_flag=True,
458
463
  help="Convert all versioned features to editable.",
459
464
  )
460
- @click.option("--force", is_flag=True, help="Bypass lifecycle state checks.")
465
+ @click.option(
466
+ "--force",
467
+ is_flag=True,
468
+ hidden=True,
469
+ help="Accepted for scripts that pass it; there is no check to bypass.",
470
+ )
461
471
  @keeps_ci_workflows
462
472
  def feature_edit(feature_name, edit_all, force):
463
473
  workspace = str(context.workspace())
@@ -135,7 +135,7 @@ def feature_xray(feature_ref, filter_cat, validate, full):
135
135
 
136
136
  data = load_toml(pyproject_path, what=f"{product} pyproject.toml")
137
137
 
138
- env = os.getenv("SPLENT_ENV", "dev")
138
+ env = context.resolve_env()
139
139
  features = read_features_from_data(data, env)
140
140
 
141
141
  # Check for stale contracts
@@ -48,11 +48,6 @@ CONTRACTS_FORMAT = 1
48
48
  # version, anything else is a pip specifier.
49
49
  RUNTIME_TABLE = ("tool", "splent", "runtime")
50
50
  RUNTIME_DEFAULTS = {"gunicorn": ">=23,<24"}
51
- RUNTIME_COMMENT = (
52
- "# What the production image installs besides the features. splent\n"
53
- "# product:build keeps splent_framework and splent_cli at the versions this\n"
54
- "# product is built with; a bare version means exactly that version.\n"
55
- )
56
51
  # Where those pins lived before, in a file of their own. Removed by the build
57
52
  # that moves them into pyproject.toml.
58
53
  LEGACY_REQUIREMENTS_FILE = "requirements.prod.txt"
@@ -449,15 +444,7 @@ def write_runtime_pins(pyproject_path: str, workspace: str) -> dict | None:
449
444
  if wanted == current:
450
445
  return None
451
446
  splent["runtime"] = wanted
452
- result = edit_toml(text, data)
453
- header = "[" + ".".join(RUNTIME_TABLE) + "]"
454
- lines = result.splitlines(keepends=True)
455
- for i, line in enumerate(lines):
456
- if line.strip() == header and not (i and lines[i - 1].lstrip().startswith("#")):
457
- newline = "\r\n" if line.endswith("\r\n") else "\n"
458
- lines[i:i] = [c + newline for c in RUNTIME_COMMENT.splitlines()]
459
- break
460
- atomic_write(pyproject_path, "".join(lines), newline="")
447
+ atomic_write(pyproject_path, edit_toml(text, data), newline="")
461
448
  return wanted
462
449
 
463
450
 
@@ -190,7 +190,7 @@ def product_config(show_all, filter_feature, filter_key, show_secrets):
190
190
  col_src = max(col_src, 6)
191
191
 
192
192
  # Header
193
- env = os.getenv("SPLENT_ENV", "dev")
193
+ env = context.resolve_env()
194
194
  feature_sources = set()
195
195
  for v in trace.values():
196
196
  src = v.get("source", "")
@@ -509,6 +509,12 @@ def product_env(generate, merge, env_name, process_all):
509
509
  "".join(env_line(k, v) for k, v in merged.items()),
510
510
  )
511
511
 
512
+ # This process may hold the file's previous values, and what follows
513
+ # in it (product:derive starting the stacks) must see the new ones.
514
+ from splent_cli.utils.dynamic_imports import reload_product_env
515
+
516
+ reload_product_env(target_env)
517
+
512
518
  if product_replaced:
513
519
  click.echo(
514
520
  click.style(" product ", dim=True)
@@ -27,10 +27,11 @@ from splent_cli.utils.proc import require_docker, run
27
27
  # ── Feature change detection ─────────────────────────────────────────
28
28
 
29
29
 
30
- def _installed_features(container_id: str) -> dict[str, str]:
30
+ def _installed_features(container_id: str) -> dict[str, dict]:
31
31
  """Read pip-installed splent_feature_* packages from the container.
32
32
 
33
- Returns {package_name: version_or_path} dict.
33
+ Returns {package_name: {"version": ..., "location": ...}}, the location
34
+ being the directory an editable install points at ("" for any other).
34
35
  """
35
36
  result = subprocess.run(
36
37
  ["docker", "exec", container_id, "pip", "list", "--format=json"],
@@ -49,7 +50,12 @@ def _installed_features(container_id: str) -> dict[str, str]:
49
50
  if name.startswith("splent-feature-") or name.startswith("splent_feature_"):
50
51
  # Normalize: pip uses hyphens, we use underscores
51
52
  normalized = name.replace("-", "_")
52
- installed[normalized] = pkg.get("version", "")
53
+ installed[normalized] = {
54
+ "version": pkg.get("version", ""),
55
+ "location": (pkg.get("editable_project_location") or "").rstrip(
56
+ "/"
57
+ ),
58
+ }
53
59
  except (json.JSONDecodeError, KeyError):
54
60
  pass
55
61
  return installed
@@ -77,6 +83,7 @@ def _declared_features(workspace: str, product: str, env: str) -> list[dict]:
77
83
  result.append(
78
84
  {
79
85
  "entry": entry,
86
+ "namespace": ns,
80
87
  "name": name,
81
88
  "version": version,
82
89
  "editable": editable,
@@ -85,25 +92,44 @@ def _declared_features(workspace: str, product: str, env: str) -> list[dict]:
85
92
  return result
86
93
 
87
94
 
95
+ def _feature_link(product: str, feat: dict) -> str:
96
+ """Container path of the product's link to a feature: features/<ns>/<name>
97
+ for an editable one (the entrypoint installs it from there, this command
98
+ from the workspace's copy), and <name>@<version> for a pinned one."""
99
+ link = f"/workspace/{product}/features/{feat['namespace']}/{feat['name']}"
100
+ return f"{link}@{feat['version']}" if feat["version"] else link
101
+
102
+
88
103
  def _detect_changes(container_id, workspace, product, env):
89
- """Compare declared features with installed ones. Returns lists of features to install."""
104
+ """The declared features the container does not have as declared:
105
+ missing, at another version, or installed from the other copy (the
106
+ workspace's for an editable feature, the product's pinned link for a
107
+ pinned one). Returns (name, path_or_entry, editable) for each."""
90
108
  installed = _installed_features(container_id)
91
109
  declared = _declared_features(workspace, product, env)
92
110
 
93
- to_install = [] # (name, path_or_entry, editable)
111
+ to_install = []
94
112
 
95
113
  for feat in declared:
96
114
  name = feat["name"]
97
- editable = feat["editable"]
115
+ found = installed.get(name)
98
116
 
99
- if editable:
100
- # Editable: check if it's installed as editable (pip shows local path)
117
+ if feat["editable"]:
101
118
  feature_path = os.path.join(workspace, name)
102
- if name not in installed or not os.path.isdir(feature_path):
119
+ copies = (f"/workspace/{name}", _feature_link(product, feat))
120
+ if (
121
+ found is None
122
+ or found["location"] not in copies
123
+ or not os.path.isdir(feature_path)
124
+ ):
103
125
  to_install.append((name, feature_path, True))
104
126
  else:
105
- # Pinned: check if version matches
106
- if name not in installed:
127
+ location = found["location"] if found else ""
128
+ if (
129
+ found is None
130
+ or found["version"] != feat["version"]
131
+ or (location and location != _feature_link(product, feat))
132
+ ):
107
133
  to_install.append((name, feat["entry"], False))
108
134
 
109
135
  return to_install
@@ -395,9 +421,12 @@ def product_restart(env_dev, env_prod, full):
395
421
  """Restart Flask inside the container.
396
422
 
397
423
  \b
398
- Detects feature changes before restarting:
399
- - New features in pyproject.toml → pip install
400
- - Features switched to editable → pip install -e
424
+ Detects feature changes before restarting, and installs each changed
425
+ feature with pip install -e from the copy pyproject.toml names:
426
+ - a feature added to pyproject.toml
427
+ - a pinned feature at another version
428
+ - a feature switched between editable (the workspace's copy) and
429
+ pinned (the product's features/ link)
401
430
  Then kills Flask/watchmedo and restarts.
402
431
 
403
432
  \b
@@ -75,7 +75,7 @@ def product_routes(filter_feature, group):
75
75
  click.secho("No routes matched.", fg="yellow")
76
76
  return
77
77
 
78
- env = os.getenv("SPLENT_ENV", "dev")
78
+ env = context.resolve_env()
79
79
 
80
80
  if group:
81
81
  # Group by feature