splent-cli 1.46.0__tar.gz → 1.47.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 (316) hide show
  1. {splent_cli-1.46.0/src/splent_cli.egg-info → splent_cli-1.47.0}/PKG-INFO +1 -1
  2. {splent_cli-1.46.0 → splent_cli-1.47.0}/pyproject.toml +4 -2
  3. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_compile.py +136 -12
  4. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_build.py +36 -1
  5. splent_cli-1.47.0/src/splent_cli/commands/product/product_frontend_audit.py +323 -0
  6. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_release.py +2 -0
  7. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_validate.py +104 -7
  8. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/frontend.py +75 -21
  9. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/preflight.py +30 -0
  10. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +21 -12
  11. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +21 -4
  12. splent_cli-1.47.0/src/splent_cli/toolchain/Dockerfile.frontend +46 -0
  13. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/package-lock.json +1300 -0
  14. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/package.json +14 -0
  15. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/splent/build.js +76 -0
  16. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/splent/legacy.js +52 -0
  17. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/splent/plugin.js +288 -0
  18. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/splent/preset.js +208 -0
  19. splent_cli-1.47.0/src/splent_cli/toolchain/frontend/splent/singletons.json +18 -0
  20. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/deploy_freshness.py +8 -3
  21. splent_cli-1.47.0/src/splent_cli/utils/frontend_check.py +866 -0
  22. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/frontend_licenses.py +4 -2
  23. splent_cli-1.47.0/src/splent_cli/utils/npm_semver.py +200 -0
  24. {splent_cli-1.46.0 → splent_cli-1.47.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  25. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli.egg-info/SOURCES.txt +11 -0
  26. {splent_cli-1.46.0 → splent_cli-1.47.0}/COPYING +0 -0
  27. {splent_cli-1.46.0 → splent_cli-1.47.0}/COPYING.LESSER +0 -0
  28. {splent_cli-1.46.0 → splent_cli-1.47.0}/README.md +0 -0
  29. {splent_cli-1.46.0 → splent_cli-1.47.0}/setup.cfg +0 -0
  30. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/__init__.py +0 -0
  31. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/__main__.py +0 -0
  32. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/cli.py +0 -0
  33. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/__init__.py +0 -0
  34. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  35. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  36. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  37. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  38. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  39. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  40. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  41. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  42. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  43. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/__init__.py +0 -0
  44. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  45. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  46. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  47. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_env.py +0 -0
  48. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_features.py +0 -0
  49. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_github.py +0 -0
  50. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  51. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_product.py +0 -0
  52. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  53. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  54. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  55. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  56. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  57. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  58. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  59. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  60. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/command/__init__.py +0 -0
  61. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/command/command_create.py +0 -0
  62. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/coverage.py +0 -0
  63. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_console.py +0 -0
  64. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  65. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  66. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  67. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  68. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  69. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  70. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_status.py +0 -0
  71. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  72. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/doctor.py +0 -0
  73. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/env/env_list.py +0 -0
  74. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/env/env_set.py +0 -0
  75. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/env/env_show.py +0 -0
  76. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/export/__init__.py +0 -0
  77. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  78. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  79. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  80. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  81. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  82. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  83. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  84. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  85. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  86. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  87. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  88. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  89. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  90. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  91. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_frontend_init.py +0 -0
  92. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  93. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  94. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  95. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  96. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  97. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  98. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  99. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  100. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  101. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_npm.py +0 -0
  102. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  103. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  104. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  105. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  106. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  107. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  108. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  109. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  110. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  111. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  112. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  113. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  114. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  115. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  116. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  117. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  118. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  119. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  120. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/lint.py +0 -0
  121. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/locust.py +0 -0
  122. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  123. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  124. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  125. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  126. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  127. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/__init__.py +0 -0
  128. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  129. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  130. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  131. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_config.py +0 -0
  132. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  133. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_console.py +0 -0
  134. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  135. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_create.py +0 -0
  136. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  137. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  138. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  139. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_down.py +0 -0
  140. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  141. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_env.py +0 -0
  142. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_list.py +0 -0
  143. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  144. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  145. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_params.py +0 -0
  146. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_port.py +0 -0
  147. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  148. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  149. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  150. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_run.py +0 -0
  151. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_select.py +0 -0
  152. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  153. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  154. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  155. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_test.py +0 -0
  156. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/product/product_up.py +0 -0
  157. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/release/__init__.py +0 -0
  158. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/release/release_core.py +0 -0
  159. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  160. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/selenium.py +0 -0
  161. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  162. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  163. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  164. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  165. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  166. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  167. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  168. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  169. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  170. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  171. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  172. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  173. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  174. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  175. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  176. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  177. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  178. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  179. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/commands/version.py +0 -0
  180. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/__init__.py +0 -0
  181. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/compose.py +0 -0
  182. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/context.py +0 -0
  183. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/credentials.py +0 -0
  184. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  185. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/editable_refresh.py +0 -0
  186. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/image_registry.py +0 -0
  187. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/index_refresh.py +0 -0
  188. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/marketplace.py +0 -0
  189. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/marketplace_api.py +0 -0
  190. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/marketplace_url.py +0 -0
  191. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/neutral_app.py +0 -0
  192. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/own_commands.py +0 -0
  193. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/pypi_quota.py +0 -0
  194. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/registry.py +0 -0
  195. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/release.py +0 -0
  196. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/release_dry_run.py +0 -0
  197. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/release_gate.py +0 -0
  198. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/spl_store.py +0 -0
  199. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/services/web_container.py +0 -0
  200. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  201. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  202. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  203. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  204. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  205. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  206. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  207. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  208. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  209. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_frontend.npmrc.j2 +0 -0
  210. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_frontend_scripts.js.j2 +0 -0
  211. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  212. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  213. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  214. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  215. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  216. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  217. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  218. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  219. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  220. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  221. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  222. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  223. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  224. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  225. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  226. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  227. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_services_service.py.j2 +0 -0
  228. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  229. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  230. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  231. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  232. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  233. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  234. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  235. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  236. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  237. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  238. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  239. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  240. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  241. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  242. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  243. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  244. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  245. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  246. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  247. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  248. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  249. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  250. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  251. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  252. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_400.html +0 -0
  253. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_401.html +0 -0
  254. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_404.html +0 -0
  255. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_500.html +0 -0
  256. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  257. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  258. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  259. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  260. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_app.css +0 -0
  261. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_app.js +0 -0
  262. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  263. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  264. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  265. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  266. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  267. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  268. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  269. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  270. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  271. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  272. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  273. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  274. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_own.css +0 -0
  275. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  276. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  277. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  278. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/__init__.py +0 -0
  279. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/archetype.py +0 -0
  280. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/cache_utils.py +0 -0
  281. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/ci_services.py +0 -0
  282. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  283. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/command_loader.py +0 -0
  284. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  285. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/db_client.py +0 -0
  286. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/db_utils.py +0 -0
  287. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/decorators.py +0 -0
  288. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  289. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/feature_installer.py +0 -0
  290. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/feature_naming.py +0 -0
  291. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/feature_params.py +0 -0
  292. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  293. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  294. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/feature_utils.py +0 -0
  295. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/forge.py +0 -0
  296. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/git_url.py +0 -0
  297. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/integrity.py +0 -0
  298. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/io_utils.py +0 -0
  299. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/lifecycle.py +0 -0
  300. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/manifest.py +0 -0
  301. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/menu_keys.py +0 -0
  302. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/migration_lock.py +0 -0
  303. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/path_utils.py +0 -0
  304. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/proc.py +0 -0
  305. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/redact.py +0 -0
  306. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  307. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/running_version.py +0 -0
  308. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/template_drift.py +0 -0
  309. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/toml_edit.py +0 -0
  310. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  311. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/uvl_diagnosis.py +0 -0
  312. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli/utils/uvl_params.py +0 -0
  313. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  314. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  315. {splent_cli-1.46.0 → splent_cli-1.47.0}/src/splent_cli.egg-info/requires.txt +0 -0
  316. {splent_cli-1.46.0 → splent_cli-1.47.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.46.0
3
+ Version: 1.47.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.46.0"
7
+ version = "1.47.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"
@@ -78,8 +78,10 @@ where = ["src"]
78
78
 
79
79
  # The templates product:create, feature:create and the rest render. They are
80
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.
81
83
  [tool.setuptools.package-data]
82
- splent_cli = ["templates/**/*"]
84
+ splent_cli = ["templates/**/*", "toolchain/**/*"]
83
85
 
84
86
  [tool.pytest.ini_options]
85
87
  testpaths = ["tests"]
@@ -1,8 +1,10 @@
1
+ import dataclasses
1
2
  import logging
2
3
  import os
3
4
  import posixpath
4
5
  import shlex
5
6
  import subprocess
7
+ from pathlib import Path
6
8
 
7
9
  import click
8
10
 
@@ -368,20 +370,43 @@ def _compile_frontend(root, feature, *, watch, production, release_build) -> boo
368
370
 
369
371
 
370
372
  def _verify_features(features, workspace: str) -> int:
371
- """feature:compile --verify: 1 when the manifest of any feature with
372
- frontend/ does not match its files, else 0. A pinned feature is checked
373
- in the copy the product links, which is what its app serves."""
373
+ """feature:compile --verify: 1 when the bundle of any feature with
374
+ frontend/ does not match its manifest, else 0.
375
+
376
+ A working copy is checked at the workspace root, a pinned feature in the
377
+ copy the product links, which is what its app serves, and where neither
378
+ is, as in the production image, in the package pip installed. When the
379
+ product has docker/frontend.lock.json (product:build writes it), every
380
+ feature it lists is checked too, and has to carry the manifest the lock
381
+ names, of the version it names: the bundle its release built.
382
+ """
383
+ from splent_cli.utils.frontend_check import read_lock
384
+
385
+ product = context.active_app(workspace)
386
+ lock = read_lock(os.path.join(workspace, product)) if product else None
387
+ locked = (lock or {}).get("features") or {}
388
+ refs = list(features)
389
+ named = {_split_ref(feature)[1] for feature in refs}
390
+ for name, entry in sorted(locked.items()):
391
+ if name not in named:
392
+ version = entry.get("version")
393
+ namespace = entry.get("namespace") or "splent_io"
394
+ refs.append(f"{namespace}/{name}" + (f"@v{version}" if version else ""))
395
+
374
396
  failed = 0
375
- for feature in features:
397
+ for feature in refs:
376
398
  namespace, base_name, version = _split_ref(feature)
377
- if version:
378
- product = context.require_app()
379
- root = os.path.join(
380
- workspace, product, "features", namespace, f"{base_name}@{version}"
399
+ entry = locked.get(base_name)
400
+ copy = _copy_of(workspace, product, namespace, base_name, version)
401
+ if copy is None and entry is not None:
402
+ failed += 1
403
+ click.secho(
404
+ f"❌ {feature}: docker/frontend.lock.json lists it, and it is "
405
+ "neither here nor installed",
406
+ fg="red",
381
407
  )
382
- else:
383
- root = os.path.join(workspace, base_name)
384
- if not frontend.declares(root):
408
+ continue
409
+ if entry is None and (copy is None or not copy.declares):
385
410
  click.echo(
386
411
  click.style(
387
412
  f" {feature} has no frontend/, nothing to verify.",
@@ -389,7 +414,11 @@ def _verify_features(features, workspace: str) -> int:
389
414
  )
390
415
  )
391
416
  continue
392
- problems = frontend.verify(root, namespace, base_name)
417
+ problems = frontend.verify_assets(
418
+ copy.assets, lock=copy.lock, production=bool(version) or copy.installed
419
+ )
420
+ if entry is not None:
421
+ problems += _against_lock(copy, entry, base_name)
393
422
  if problems:
394
423
  failed += 1
395
424
  click.secho(
@@ -402,6 +431,101 @@ def _verify_features(features, workspace: str) -> int:
402
431
  return 1 if failed else 0
403
432
 
404
433
 
434
+ @dataclasses.dataclass
435
+ class _Copy:
436
+ """The copy of a feature --verify checks: its assets/, its frontend
437
+ lock (a package pip installed has none), whether pip installed it, and
438
+ whether it declares frontend/ (an installed one does when it carries a
439
+ manifest)."""
440
+
441
+ assets: Path
442
+ lock: Path | None
443
+ installed: bool
444
+ declares: bool
445
+
446
+
447
+ def _copy_of(workspace, product, namespace, base_name, version) -> _Copy | None:
448
+ """The working copy, the product's link to a pinned release, or else
449
+ the package pip installed. None when there is none."""
450
+ if version:
451
+ root = (
452
+ os.path.join(
453
+ workspace, product, "features", namespace, f"{base_name}@{version}"
454
+ )
455
+ if product
456
+ else None
457
+ )
458
+ else:
459
+ root = os.path.join(workspace, base_name)
460
+ if root and os.path.isdir(root):
461
+ return _Copy(
462
+ frontend.assets_dir(root, namespace, base_name),
463
+ Path(root) / frontend.FRONTEND_DIR / frontend.LOCK,
464
+ False,
465
+ frontend.declares(root),
466
+ )
467
+ import importlib.util
468
+
469
+ try:
470
+ spec = importlib.util.find_spec(f"{namespace}.{base_name}")
471
+ except (ImportError, ValueError):
472
+ spec = None
473
+ if spec is None or not spec.submodule_search_locations:
474
+ return None
475
+ assets = Path(list(spec.submodule_search_locations)[0]) / "assets"
476
+ return _Copy(assets, None, True, (assets / "meta" / "manifest.json").is_file())
477
+
478
+
479
+ def _against_lock(copy: _Copy, entry: dict, base_name: str) -> list[str]:
480
+ """What does not match between a feature's bundle and its entry in
481
+ docker/frontend.lock.json."""
482
+ problems = []
483
+ wanted = entry.get("manifest_sha256")
484
+ found = frontend.manifest_sha256(copy.assets)
485
+ if wanted and found and found != wanted:
486
+ problems.append(
487
+ "assets/meta/manifest.json is not the one docker/frontend.lock.json "
488
+ "names: run splent product:build"
489
+ )
490
+ if not copy.installed:
491
+ return problems
492
+ import importlib.metadata
493
+
494
+ distribution = entry.get("distribution") or base_name
495
+ try:
496
+ installed = importlib.metadata.version(distribution)
497
+ except importlib.metadata.PackageNotFoundError:
498
+ installed = None
499
+ if entry.get("version") and installed != entry["version"]:
500
+ problems.append(
501
+ f"pip installed {installed or 'no version'}, and "
502
+ f"docker/frontend.lock.json names {entry['version']}"
503
+ )
504
+ if found is None:
505
+ problems.append(
506
+ f"it was installed {_channel(distribution)}, without the bundle its "
507
+ "release built: release it again, so its wheel and its tag carry it"
508
+ )
509
+ return problems
510
+
511
+
512
+ def _channel(distribution: str) -> str:
513
+ """How pip installed DISTRIBUTION: from its git tag (git+) or a wheel."""
514
+ import importlib.metadata
515
+ import json
516
+
517
+ try:
518
+ direct = importlib.metadata.distribution(distribution).read_text(
519
+ "direct_url.json"
520
+ )
521
+ url = json.loads(direct or "{}")
522
+ except (importlib.metadata.PackageNotFoundError, ValueError):
523
+ url = {}
524
+ if "vcs_info" in url:
525
+ return f"from its git tag (git+{url.get('url', '')})"
526
+ return "from a wheel"
527
+
528
+
405
529
  def _feature_ref(name: str) -> str:
406
530
  """*name* as typed, with the ``splent_feature_`` prefix on the feature's
407
531
  own name: ``auth`` and ``splent_io/auth`` are ``splent_feature_auth``
@@ -62,6 +62,10 @@ LEGACY_REQUIREMENTS_FILE = "requirements.prod.txt"
62
62
  # Dockerfile applies it as a constraint on every pip install, so two builds of
63
63
  # one commit install the same set. Versioned in docker/.
64
64
  LOCK_FILE = "requirements.lock"
65
+ # What the features' bundles are and ship, which the image verifies the
66
+ # installed ones against (splent_cli.utils.frontend_check). Versioned in
67
+ # docker/ too.
68
+ FRONTEND_LOCK_FILE = "frontend.lock.json"
65
69
  LOCK_HEADER = (
66
70
  "# Every third-party package the production image installs, at the version\n"
67
71
  "# the last build resolved. Written by splent product:build and\n"
@@ -1202,6 +1206,30 @@ def _contracts_snapshot(workspace: str, declared_features: Sequence[str]) -> dic
1202
1206
  return {"format": CONTRACTS_FORMAT, "features": features}
1203
1207
 
1204
1208
 
1209
+ def write_frontend_lock(
1210
+ workspace: str, product_path: str, data: dict, declared_features: Sequence[str]
1211
+ ) -> str:
1212
+ """Write docker/frontend.lock.json for the features the image installs.
1213
+
1214
+ For each one with frontend/, the release, the sha256 of the manifest its
1215
+ bundle carries and what the bundle ships, with the provider that
1216
+ satisfies each of its peers; the shell; and the features still built in
1217
+ legacy mode (splent_cli.utils.frontend_check.lock). The production
1218
+ image checks every bundle pip installed against it, and the product's
1219
+ CI audits what they ship from it. Returns its path.
1220
+ """
1221
+ from splent_cli.utils import frontend_check
1222
+
1223
+ check = frontend_check.check_product(workspace, product_path, declared_features)
1224
+ path = os.path.join(product_path, "docker", FRONTEND_LOCK_FILE)
1225
+ atomic_write(
1226
+ path,
1227
+ frontend_check.lock_text(frontend_check.lock(check, data)),
1228
+ dir_owner=True,
1229
+ )
1230
+ return path
1231
+
1232
+
1205
1233
  def _not_shipped(directory, names):
1206
1234
  """Bytecode a local run of a feature left behind, never part of the bundle."""
1207
1235
  return {n for n in names if n == "__pycache__" or n.endswith(".pyc")}
@@ -1481,7 +1509,8 @@ def write_deploy_artifacts(
1481
1509
  * ``[tool.splent.runtime]`` in pyproject.toml, the framework and CLI the
1482
1510
  image installs from PyPI;
1483
1511
  * ``splent.contracts.json`` and ``docker/spl/``, the refinement metadata
1484
- and the UVL model the image reads.
1512
+ and the UVL model the image reads;
1513
+ * ``docker/frontend.lock.json``, the bundles the image verifies.
1485
1514
 
1486
1515
  All of them are versioned with the product. product:build and
1487
1516
  product:release both go through here, so a release commits exactly what
@@ -1791,6 +1820,12 @@ def write_deploy_artifacts(
1791
1820
  written.append(CONTRACTS_SNAPSHOT)
1792
1821
  click.echo(f"✅ Created: {contracts_path}")
1793
1822
 
1823
+ frontend_lock = write_frontend_lock(
1824
+ workspace, product_path, data, declared_features
1825
+ )
1826
+ written.append(f"docker/{FRONTEND_LOCK_FILE}")
1827
+ click.echo(f"✅ Created: {frontend_lock}")
1828
+
1794
1829
  staged_model = stage_spl_model(workspace, product, docker_path)
1795
1830
  if staged_model:
1796
1831
  written.append(f"docker/{SPL_STAGING_DIR}")
@@ -0,0 +1,323 @@
1
+ """
2
+ product:frontend:audit — what the product's bundles ship, audited as one set.
3
+
4
+ Each feature with frontend/ audits its own lock when it is released (npm
5
+ audit, services/frontend.py), but a bundle is immutable: an advisory
6
+ published later reaches the product only through a new release of every
7
+ feature that ships the package. This audits the union of what the bundles
8
+ ship, as docker/frontend.lock.json records it (product:build writes it),
9
+ against npm's advisory database, and names the features to release again.
10
+
11
+ npm audit needs a lock it could install, and the product has none, so the
12
+ packages go straight to the registry's bulk advisory endpoint, the one npm
13
+ audit asks, and each advisory is matched against the shipped version here.
14
+ A critical advisory fails, as it fails a feature's release; a high one is
15
+ said; an audit that cannot reach the registry fails too. --sbom writes the
16
+ CycloneDX SBOM of the whole: the product, each feature and what each one
17
+ ships, with what the features vendor outside npm when their snapshot is at
18
+ hand.
19
+ """
20
+
21
+ import datetime
22
+ import json
23
+ import os
24
+ import uuid
25
+
26
+ import click
27
+
28
+ from splent_cli.services import compose, context, frontend
29
+ from splent_cli.utils.frontend_check import LOCK_FILE, read_lock
30
+ from splent_cli.utils.npm_semver import satisfies
31
+
32
+ DEFAULT_REGISTRY = "https://registry.npmjs.org"
33
+ ADVISORIES = "/-/npm/v1/security/advisories/bulk"
34
+ REQUEST_TIMEOUT = 30
35
+ BLOCKING = ("critical",)
36
+ WARNING = ("high",)
37
+
38
+
39
+ def shipped(lock: dict) -> dict[str, dict[str, list[str]]]:
40
+ """``{package: {version: [features]}}`` of what the bundles in LOCK ship."""
41
+ found: dict[str, dict[str, list[str]]] = {}
42
+ for name, feature in sorted((lock.get("features") or {}).items()):
43
+ short = name.removeprefix("splent_feature_")
44
+ for package, described in (feature.get("bundled") or {}).items():
45
+ for one in str(described).split(", "):
46
+ version = one.partition(" ")[0]
47
+ found.setdefault(package, {}).setdefault(version, []).append(short)
48
+ return found
49
+
50
+
51
+ def registry() -> str:
52
+ """The npm registry, as npm's own setting names it."""
53
+ return (os.getenv("NPM_CONFIG_REGISTRY") or DEFAULT_REGISTRY).rstrip("/")
54
+
55
+
56
+ def advisories(packages: dict) -> dict:
57
+ """The registry's advisories for PACKAGES, ``{package: [advisory]}``.
58
+ Raises click.ClickException when the registry cannot be asked."""
59
+ import requests
60
+
61
+ body = {name: sorted(versions) for name, versions in packages.items()}
62
+ try:
63
+ response = requests.post(
64
+ registry() + ADVISORIES, json=body, timeout=REQUEST_TIMEOUT
65
+ )
66
+ except requests.RequestException as e:
67
+ raise click.ClickException(f"The npm registry could not be asked: {e}")
68
+ if response.status_code != 200:
69
+ raise click.ClickException(
70
+ f"The npm registry answered {response.status_code} to the audit."
71
+ )
72
+ try:
73
+ return response.json()
74
+ except ValueError:
75
+ raise click.ClickException("The npm registry's answer is not JSON.")
76
+
77
+
78
+ def findings(packages: dict, found: dict) -> list[dict]:
79
+ """Each advisory of FOUND that covers a version PACKAGES ships, with the
80
+ features that ship it, most severe first."""
81
+ order = {"critical": 0, "high": 1, "moderate": 2, "low": 3, "info": 4}
82
+ result = []
83
+ for name, versions in packages.items():
84
+ for advisory in found.get(name) or []:
85
+ vulnerable = advisory.get("vulnerable_versions") or "*"
86
+ for version, features in sorted(versions.items()):
87
+ try:
88
+ covered = satisfies(version, vulnerable)
89
+ except ValueError:
90
+ covered = True
91
+ if covered:
92
+ result.append(
93
+ {
94
+ "package": name,
95
+ "version": version,
96
+ "features": features,
97
+ "severity": advisory.get("severity") or "unknown",
98
+ "title": advisory.get("title") or "",
99
+ "url": advisory.get("url") or "",
100
+ }
101
+ )
102
+ return sorted(
103
+ result, key=lambda f: (order.get(f["severity"], 5), f["package"], f["version"])
104
+ )
105
+
106
+
107
+ def sbom(lock: dict, workspace: str, product: str, version: str) -> dict:
108
+ """The CycloneDX 1.5 SBOM of the product's bundles: a component for the
109
+ product, one for each feature and one for each package a bundle ships,
110
+ each feature depending on what it ships."""
111
+ from splent_cli.utils.running_version import cli_version
112
+
113
+ components: dict[str, dict] = {}
114
+ dependencies: list[dict] = []
115
+ for name, feature in sorted((lock.get("features") or {}).items()):
116
+ distribution = feature.get("distribution") or name
117
+ ref = f"pkg:pypi/{distribution}@{feature.get('version') or 'editable'}"
118
+ components[ref] = {
119
+ "type": "library",
120
+ "bom-ref": ref,
121
+ "name": distribution,
122
+ "version": feature.get("version") or "editable",
123
+ "purl": ref,
124
+ "properties": [
125
+ {
126
+ "name": "splent:manifest_sha256",
127
+ "value": feature.get("manifest_sha256"),
128
+ }
129
+ ],
130
+ }
131
+ depends = []
132
+ for package, described in sorted((feature.get("bundled") or {}).items()):
133
+ for one in str(described).split(", "):
134
+ version_, _, license_ = one.partition(" ")
135
+ purl = frontend._purl(package, version_)
136
+ depends.append(purl)
137
+ components.setdefault(
138
+ purl,
139
+ {
140
+ "type": "library",
141
+ "bom-ref": purl,
142
+ "name": package,
143
+ "version": version_,
144
+ "purl": purl,
145
+ "licenses": frontend._cyclonedx_licenses(
146
+ None if license_ in ("", "UNKNOWN") else license_
147
+ ),
148
+ },
149
+ )
150
+ for vendored in _vendored(workspace, feature, name):
151
+ depends.append(vendored["bom-ref"])
152
+ components.setdefault(vendored["bom-ref"], vendored)
153
+ dependencies.append({"ref": ref, "dependsOn": sorted(set(depends))})
154
+ product_ref = f"pkg:generic/{product}@{version}"
155
+ dependencies.insert(
156
+ 0, {"ref": product_ref, "dependsOn": [d["ref"] for d in dependencies]}
157
+ )
158
+ return {
159
+ "bomFormat": "CycloneDX",
160
+ "specVersion": "1.5",
161
+ "serialNumber": f"urn:uuid:{uuid.uuid4()}",
162
+ "version": 1,
163
+ "metadata": {
164
+ "timestamp": datetime.datetime.now(datetime.timezone.utc)
165
+ .replace(microsecond=0)
166
+ .isoformat(),
167
+ "tools": {
168
+ "components": [
169
+ {
170
+ "type": "application",
171
+ "name": "splent_cli",
172
+ "version": cli_version(),
173
+ }
174
+ ]
175
+ },
176
+ "component": {
177
+ "type": "application",
178
+ "bom-ref": product_ref,
179
+ "name": product,
180
+ "version": version,
181
+ },
182
+ },
183
+ "components": list(components.values()),
184
+ "dependencies": dependencies,
185
+ }
186
+
187
+
188
+ def _vendored(workspace: str, feature: dict, name: str) -> list[dict]:
189
+ """The components a feature's release SBOM lists as vendored (files of
190
+ a third party that come from no npm package), when its copy is here."""
191
+ entry = f"{feature.get('namespace') or 'splent_io'}/{name}"
192
+ if feature.get("version"):
193
+ entry += f"@v{feature['version']}"
194
+ root = compose.feature_dir(workspace, entry)
195
+ assets = frontend.assets_dir(root, feature.get("namespace") or "splent_io", name)
196
+ try:
197
+ document = json.loads((assets / "meta" / "sbom.cdx.json").read_text())
198
+ except (OSError, ValueError):
199
+ return []
200
+ return [
201
+ component
202
+ for component in document.get("components") or []
203
+ if {"name": "splent:origin", "value": "vendored"}
204
+ in (component.get("properties") or [])
205
+ ]
206
+
207
+
208
+ @click.command(
209
+ "product:frontend:audit",
210
+ short_help="Audit what the product's bundles ship against npm's advisories.",
211
+ )
212
+ @click.option(
213
+ "--sbom",
214
+ "sbom_path",
215
+ default=None,
216
+ type=click.Path(dir_okay=False, writable=True),
217
+ help="Also write the CycloneDX SBOM of the product's bundles to this file.",
218
+ )
219
+ def product_frontend_audit(sbom_path):
220
+ """
221
+ Audit the packages the product's bundles ship, as one set.
222
+
223
+ \b
224
+ Reads docker/frontend.lock.json, which product:build writes, and asks
225
+ npm's registry for the advisories of every package any bundle ships.
226
+ Each one that covers a shipped version is listed with the features
227
+ that ship it, which a fix reaches only through a new release of each.
228
+ A critical advisory fails, a high one is said, the rest are counted.
229
+ An audit that cannot reach the registry fails too.
230
+
231
+ \b
232
+ --sbom writes the CycloneDX SBOM of the whole: the product, each
233
+ feature and what it ships.
234
+
235
+ \b
236
+ Examples:
237
+ splent product:frontend:audit
238
+ splent product:frontend:audit --sbom frontend.sbom.cdx.json
239
+ """
240
+ product = context.require_app()
241
+ workspace = str(context.workspace())
242
+ product_dir = os.path.join(workspace, product)
243
+ lock = read_lock(product_dir)
244
+ if lock is None:
245
+ raise click.ClickException(
246
+ f"{product} has no docker/{LOCK_FILE}. Run splent product:build, which "
247
+ "writes it."
248
+ )
249
+
250
+ packages = shipped(lock)
251
+ count = sum(len(versions) for versions in packages.values())
252
+ click.echo()
253
+ click.echo(
254
+ click.style(f" Auditing what the bundles of {product} ship...", bold=True)
255
+ )
256
+ click.echo()
257
+
258
+ if sbom_path:
259
+ document = sbom(lock, workspace, product, _product_version(product_dir))
260
+ with open(sbom_path, "w", encoding="utf-8") as handle:
261
+ handle.write(json.dumps(document, indent=2) + "\n")
262
+ click.secho(
263
+ f" ℹ️ SBOM of {len(document['components'])} component(s) written to "
264
+ f"{sbom_path}",
265
+ fg="bright_black",
266
+ )
267
+
268
+ failed = False
269
+ if packages:
270
+ found = findings(packages, advisories(packages))
271
+ for item in found:
272
+ line = (
273
+ f"{item['package']} {item['version']} ({item['severity']}): "
274
+ f"{item['title']} {item['url']}".rstrip()
275
+ + f". Shipped by {', '.join(item['features'])}, to release again "
276
+ "with a fixed version."
277
+ )
278
+ if item["severity"] in BLOCKING:
279
+ click.secho(f" ❌ {line}", fg="red")
280
+ failed = True
281
+ elif item["severity"] in WARNING:
282
+ click.secho(f" ⚠️ {line}", fg="yellow")
283
+ rest = [f for f in found if f["severity"] not in BLOCKING + WARNING]
284
+ if rest:
285
+ click.secho(
286
+ f" ℹ️ {len(rest)} advisory(ies) of lower severity, which do not "
287
+ "fail the audit",
288
+ fg="bright_black",
289
+ )
290
+ if not found:
291
+ click.secho(
292
+ f" ✅ No advisory covers the {count} package version(s) the "
293
+ f"bundles of {len(lock.get('features') or {})} feature(s) ship",
294
+ fg="green",
295
+ )
296
+ else:
297
+ click.secho(
298
+ " ✅ No bundle of this product ships an npm package", fg="green"
299
+ )
300
+
301
+ click.echo()
302
+ if failed:
303
+ click.secho(
304
+ " ❌ A bundle ships a package with a critical advisory.",
305
+ fg="red",
306
+ bold=True,
307
+ )
308
+ click.echo()
309
+ raise SystemExit(1)
310
+
311
+
312
+ def _product_version(product_dir: str) -> str:
313
+ import tomllib
314
+
315
+ try:
316
+ with open(os.path.join(product_dir, "pyproject.toml"), "rb") as handle:
317
+ data = tomllib.load(handle)
318
+ except (OSError, tomllib.TOMLDecodeError):
319
+ return "unknown"
320
+ return str(data.get("project", {}).get("version") or "unknown")
321
+
322
+
323
+ cli_command = product_frontend_audit
@@ -11,6 +11,7 @@ import tempfile
11
11
  import click
12
12
  from splent_cli.commands.product.product_build import (
13
13
  CONTRACTS_SNAPSHOT,
14
+ FRONTEND_LOCK_FILE,
14
15
  LOCK_FILE,
15
16
  SPL_STAGING_DIR,
16
17
  build_production_image,
@@ -126,6 +127,7 @@ RELEASE_ARTIFACTS = (
126
127
  # before they exist.
127
128
  f"{deploy_bundle.BUNDLE_DIR}/",
128
129
  f"docker/{LOCK_FILE}",
130
+ f"docker/{FRONTEND_LOCK_FILE}",
129
131
  CONTRACTS_SNAPSHOT,
130
132
  f"docker/{SPL_STAGING_DIR}/",
131
133
  )