simple-module-cli 0.0.40__tar.gz → 0.0.42__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 (189) hide show
  1. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/PKG-INFO +1 -1
  2. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/pyproject.toml +1 -1
  3. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/cli.py +17 -0
  4. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/docker_assets.py +1 -1
  5. simple_module_cli-0.0.42/simple_module_cli/migration_recipes/mail_settings.py +48 -0
  6. simple_module_cli-0.0.42/simple_module_cli/migration_recipes/users_tokens.py +34 -0
  7. simple_module_cli-0.0.42/simple_module_cli/migrations_cmd.py +66 -0
  8. simple_module_cli-0.0.42/simple_module_cli/package_update.py +189 -0
  9. simple_module_cli-0.0.42/simple_module_cli/package_update_deps.py +153 -0
  10. simple_module_cli-0.0.42/simple_module_cli/package_update_npm.py +126 -0
  11. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-cli/SKILL.md +21 -3
  12. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +2 -0
  13. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +1 -0
  14. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +4 -0
  15. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/dockerignore +11 -1
  16. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/host-flat.Dockerfile +3 -2
  17. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/host.Dockerfile +4 -2
  18. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/worker.Dockerfile.tpl +3 -1
  19. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/README.md.tpl +18 -0
  20. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/conftest.py +14 -0
  21. simple_module_cli-0.0.42/tests/test_cli_docker_persistence_secrets.py +94 -0
  22. simple_module_cli-0.0.42/tests/test_cli_package_update_groups.py +74 -0
  23. simple_module_cli-0.0.42/tests/test_cli_package_update_npm.py +220 -0
  24. simple_module_cli-0.0.42/tests/test_cli_prepare_data_migrations.py +185 -0
  25. simple_module_cli-0.0.42/tests/test_cli_prepare_data_postgres.py +89 -0
  26. simple_module_cli-0.0.40/simple_module_cli/package_update.py +0 -262
  27. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/.gitignore +0 -0
  28. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/LICENSE +0 -0
  29. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/README.md +0 -0
  30. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/__init__.py +0 -0
  31. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_add_install.py +0 -0
  32. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_add_sources.py +0 -0
  33. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_app_project_manifests.py +0 -0
  34. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_env.py +0 -0
  35. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_local_env.py +0 -0
  36. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_module_build.py +0 -0
  37. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_module_host.py +0 -0
  38. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/_templating.py +0 -0
  39. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/add_cmd.py +0 -0
  40. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/app_project.py +0 -0
  41. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/case.py +0 -0
  42. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/catalog.py +0 -0
  43. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/docker_cmd.py +0 -0
  44. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/framework_repo.py +0 -0
  45. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/git_source.py +0 -0
  46. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/module_cmd.py +0 -0
  47. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/new.py +0 -0
  48. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/openapi_cmd.py +0 -0
  49. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/openapi_types.py +0 -0
  50. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/pins.py +0 -0
  51. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/plugins.py +0 -0
  52. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/pypi.py +0 -0
  53. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/pyproject_edit.py +0 -0
  54. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/recipes.py +0 -0
  55. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/requirements.py +0 -0
  56. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/scaffolding.py +0 -0
  57. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/README.md +0 -0
  58. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
  59. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
  60. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
  61. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
  62. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
  63. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
  64. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
  65. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
  66. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
  67. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/skills_cmd.py +0 -0
  68. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/Makefile.snippet +0 -0
  69. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +0 -0
  70. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/docker/compose-worker.yml.tpl +0 -0
  71. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/.env.example +0 -0
  72. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/.gitignore +0 -0
  73. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/Makefile +0 -0
  74. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
  75. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
  76. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/alembic.ini +0 -0
  77. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
  78. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
  79. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/module-assets.ts +0 -0
  80. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
  81. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
  82. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
  83. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
  84. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/styles.css +0 -0
  85. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
  86. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
  87. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/client_app/vite.dev-url.ts +0 -0
  88. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/main.py +0 -0
  89. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/migrations/env.py +0 -0
  90. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
  91. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
  92. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
  93. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/routes.py +0 -0
  94. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/host/templates/index.html +0 -0
  95. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
  96. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
  97. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/.gitignore +0 -0
  98. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/README.md.tpl +0 -0
  99. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  100. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/constants.py.tpl +0 -0
  101. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/contracts/__init__.py.tpl +0 -0
  102. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/contracts/schemas.py.tpl +0 -0
  103. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/deps.py.tpl +0 -0
  104. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  105. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
  106. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +0 -0
  107. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/locales/en.json.tpl +0 -0
  108. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/models.py.tpl +0 -0
  109. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
  110. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/Browse.tsx.tpl +0 -0
  111. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/Create.tsx.tpl +0 -0
  112. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/Edit.tsx.tpl +0 -0
  113. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/api.generated.ts.tpl +0 -0
  114. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/pages/api.ts.tpl +0 -0
  115. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/py.typed +0 -0
  116. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/service.py.tpl +0 -0
  117. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
  118. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
  119. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/LICENSE +0 -0
  120. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/README.md.tpl +0 -0
  121. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/__PACKAGE__/pages/Browse.tsx.tpl +0 -0
  122. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/__PACKAGE__/pages/Create.tsx.tpl +0 -0
  123. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/__PACKAGE__/pages/Edit.tsx.tpl +0 -0
  124. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/framework_repo/pyproject.toml.tpl +0 -0
  125. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +0 -0
  126. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
  127. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/package.json.tpl +0 -0
  128. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
  129. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/tests/test___PACKAGE__.py.tpl +0 -0
  130. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
  131. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/.env.example +0 -0
  132. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/.gitignore +0 -0
  133. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/Makefile +0 -0
  134. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
  135. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
  136. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
  137. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/update_cmd.py +0 -0
  138. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/uv_workspace.py +0 -0
  139. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/simple_module_cli/wizard.py +0 -0
  140. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_build_packaging.py +0 -0
  141. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_case.py +0 -0
  142. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_add_cmd.py +0 -0
  143. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_add_spec_parse.py +0 -0
  144. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_add_workspace_member.py +0 -0
  145. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_asset_prerequisites.py +0 -0
  146. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_catalog.py +0 -0
  147. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_catalog_depends_on.py +0 -0
  148. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_docker_assets.py +0 -0
  149. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_docker_workers.py +0 -0
  150. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_gen_types_scaffold.py +0 -0
  151. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_git_scan.py +0 -0
  152. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_new.py +0 -0
  153. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_new_dest_tolerance.py +0 -0
  154. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_new_git_module.py +0 -0
  155. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_new_migration_bootstrap.py +0 -0
  156. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_new_regressions.py +0 -0
  157. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_new_scaffold_layout.py +0 -0
  158. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_openapi_export.py +0 -0
  159. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_openapi_gen_types.py +0 -0
  160. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_package_update.py +0 -0
  161. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_package_update_pins.py +0 -0
  162. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_pyproject_edit.py +0 -0
  163. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_recipes.py +0 -0
  164. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_update_sources.py +0 -0
  165. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_vite_port_env.py +0 -0
  166. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_wizard.py +0 -0
  167. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_cli_wizard_git.py +0 -0
  168. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_create_module_ci_skip.py +0 -0
  169. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_env_helper.py +0 -0
  170. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_framework_repo_wiring.py +0 -0
  171. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_cmd_build.py +0 -0
  172. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_cmd_e2e.py +0 -0
  173. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_cmd_verify.py +0 -0
  174. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_css_assets.py +0 -0
  175. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_css_build.py +0 -0
  176. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_css_subdir_sources.py +0 -0
  177. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_npm_aliases.py +0 -0
  178. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_module_pages_manifest.py +0 -0
  179. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_no_framework_deps.py +0 -0
  180. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_plugin_discovery.py +0 -0
  181. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffold_app_tsx_spa_links.py +0 -0
  182. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffold_rollback.py +0 -0
  183. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffolded_host_setup_wizard.py +0 -0
  184. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffolding_framework_repo.py +0 -0
  185. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffolding_host.py +0 -0
  186. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffolding_module.py +0 -0
  187. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_scaffolding_module_js.py +0 -0
  188. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_skills_cmd.py +0 -0
  189. {simple_module_cli-0.0.40 → simple_module_cli-0.0.42}/tests/test_template_requires_framework.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_cli
3
- Version: 0.0.40
3
+ Version: 0.0.42
4
4
  Summary: Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host.
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_cli"
3
- version = "0.0.40"
3
+ version = "0.0.42"
4
4
  description = "Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -23,6 +23,7 @@ import typer
23
23
  from simple_module_cli.add_cmd import add_module, run_add
24
24
  from simple_module_cli.case import to_kebab_case
25
25
  from simple_module_cli.docker_cmd import docker_app
26
+ from simple_module_cli.migrations_cmd import migrations_app
26
27
  from simple_module_cli.module_cmd import module_app
27
28
  from simple_module_cli.new import new_project
28
29
  from simple_module_cli.openapi_cmd import openapi_app
@@ -49,6 +50,22 @@ app.add_typer(docker_app, name="docker")
49
50
  app.add_typer(skills_app, name="skills")
50
51
  app.add_typer(module_app, name="module")
51
52
  app.add_typer(openapi_app, name="openapi")
53
+ app.add_typer(migrations_app, name="migrations")
54
+
55
+
56
+ @app.command("doctor")
57
+ def doctor() -> None:
58
+ """Check installed modules and their database compatibility."""
59
+ try:
60
+ from simple_module_core.__main__ import main as run_doctor
61
+ except ImportError:
62
+ # The CLI installs on its own (to scaffold); doctor needs a project's framework.
63
+ typer.echo(
64
+ "ERROR: smpy doctor needs simple_module_core; run it inside a project.", err=True
65
+ )
66
+ raise typer.Exit(1) from None
67
+
68
+ raise typer.Exit(run_doctor())
52
69
 
53
70
 
54
71
  @app.command("create-host")
@@ -78,7 +78,7 @@ def scaffold_docker_assets(target: Path, ctx: ScaffoldCtx, *, flat: bool = False
78
78
  .replace(_PG_DB_TOKEN, pg_db)
79
79
  .replace(_APP_EXTRA_ENV_TOKEN, _APP_BROKER_ENV if with_tasks else "")
80
80
  )
81
- volumes = ["pgdata"] if db == "postgres" else ["appdata"]
81
+ volumes = ["appdata", "pgdata"] if db == "postgres" else ["appdata"]
82
82
  if with_tasks:
83
83
  compose += _read(templates / f"compose-tasks-{db}.yml.tpl").replace(_PG_DB_TOKEN, pg_db)
84
84
  volumes.append("redisdata")
@@ -0,0 +1,48 @@
1
+ """Frozen system mail-settings move from d7b3a9e5c1f2 (#711).
2
+
3
+ An existing target value wins; the obsolete source row is removed.
4
+ """
5
+
6
+ import sqlalchemy as sa
7
+ from alembic import op
8
+
9
+ _TABLE = "settings_setting"
10
+ _SCOPE_SYSTEM = "system"
11
+ _SYSTEM_SCOPE_ID = ""
12
+
13
+ _KEY_PAIRS = (
14
+ ("users.mailer", "mail.backend"),
15
+ *((f"users.smtp_{f}", f"mail.smtp_{f}") for f in ("host", "port", "username", "password")),
16
+ ("users.smtp_from", "mail.smtp_from"),
17
+ ("users.smtp_tls", "mail.smtp_tls"),
18
+ )
19
+
20
+
21
+ def _rename(old: str, new: str) -> None:
22
+ scope = {"scope": _SCOPE_SYSTEM, "scope_id": _SYSTEM_SCOPE_ID, "old": old, "new": new}
23
+ bind = op.get_bind()
24
+ bind.execute(
25
+ sa.text(
26
+ f"DELETE FROM {_TABLE} WHERE scope = :scope AND scope_id = :scope_id AND key = :old"
27
+ f" AND EXISTS (SELECT 1 FROM {_TABLE} AS t WHERE t.scope = :scope"
28
+ " AND t.scope_id = :scope_id AND t.key = :new)"
29
+ ),
30
+ scope,
31
+ )
32
+ bind.execute(
33
+ sa.text(
34
+ f"UPDATE {_TABLE} SET key = :new"
35
+ " WHERE scope = :scope AND scope_id = :scope_id AND key = :old"
36
+ ),
37
+ scope,
38
+ )
39
+
40
+
41
+ def upgrade() -> None:
42
+ for old, new in _KEY_PAIRS:
43
+ _rename(old, new)
44
+
45
+
46
+ def downgrade() -> None:
47
+ for old, new in _KEY_PAIRS:
48
+ _rename(new, old)
@@ -0,0 +1,34 @@
1
+ """Frozen token-storage upgrade from a626b1c8d023 (#711).
2
+
3
+ Upgrade and downgrade invalidate all access/refresh credentials. Stop all old
4
+ application workers before upgrading; clients must sign in again.
5
+ """
6
+
7
+ import sqlalchemy as sa
8
+ from alembic import op
9
+
10
+
11
+ def _clear_tokens() -> None:
12
+ for table in ("users_refresh_token", "users_access_token"):
13
+ op.execute(sa.table(table).delete())
14
+
15
+
16
+ def upgrade() -> None:
17
+ _clear_tokens()
18
+ with op.batch_alter_table("users_refresh_token") as batch:
19
+ batch.alter_column(
20
+ "token", existing_type=sa.Uuid(), type_=sa.String(64), postgresql_using="token::text"
21
+ )
22
+ with op.batch_alter_table("users_access_token") as batch:
23
+ batch.alter_column("token", existing_type=sa.String(43), type_=sa.String(64))
24
+
25
+
26
+ def downgrade() -> None:
27
+ # Digests cannot be restored to UUIDs or bearer secrets.
28
+ _clear_tokens()
29
+ with op.batch_alter_table("users_refresh_token") as batch:
30
+ batch.alter_column(
31
+ "token", existing_type=sa.String(64), type_=sa.Uuid(), postgresql_using="token::uuid"
32
+ )
33
+ with op.batch_alter_table("users_access_token") as batch:
34
+ batch.alter_column("token", existing_type=sa.String(64), type_=sa.String(43))
@@ -0,0 +1,66 @@
1
+ """Prepare frozen framework upgrades in a consuming host's own Alembic graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+ from uuid import uuid4
10
+
11
+ import typer
12
+
13
+ migrations_app = typer.Typer(help="Prepare host-owned framework upgrade revisions.")
14
+ _RECIPES = {
15
+ "mail.settings_namespace_v1": ("mail", "mail_settings.py"),
16
+ "users.hashed_bearer_tokens_v1": ("users", "users_tokens.py"),
17
+ }
18
+
19
+
20
+ @migrations_app.command("prepare")
21
+ def prepare(
22
+ upgrade_id: Annotated[str, typer.Argument(help="Stable framework upgrade id.")],
23
+ head: Annotated[str, typer.Option("--head", help="Explicit host head or label@head.")],
24
+ config: Annotated[Path | None, typer.Option("--config", help="Host alembic.ini.")] = None,
25
+ ) -> None:
26
+ """Write a self-contained revision; never connect to or migrate the database."""
27
+ if upgrade_id not in _RECIPES:
28
+ raise typer.BadParameter(f"Unknown upgrade {upgrade_id!r}; choose {', '.join(_RECIPES)}")
29
+ try:
30
+ from alembic.config import Config
31
+ from alembic.script import ScriptDirectory
32
+ from alembic.util.exc import CommandError
33
+ except ImportError as exc:
34
+ raise typer.BadParameter(
35
+ "Run this command in the host environment with Alembic installed"
36
+ ) from exc
37
+
38
+ root = Path(os.environ.get("SM_PROJECT_ROOT", Path.cwd()))
39
+ if config is None:
40
+ config = root / "alembic.ini"
41
+ if not config.is_file():
42
+ config = root / "host/alembic.ini"
43
+ if not config.is_file():
44
+ raise typer.BadParameter(f"Host Alembic config not found: {config}")
45
+ try:
46
+ script = ScriptDirectory.from_config(Config(str(config)))
47
+ parent = script.get_revision(head)
48
+ if parent is None or not parent.is_head:
49
+ raise typer.BadParameter("--head must resolve to one existing host head")
50
+ label, recipe = _RECIPES[upgrade_id]
51
+ existing = {name for rev in script.walk_revisions() for name in rev.branch_labels}
52
+ except CommandError as exc:
53
+ raise typer.BadParameter(str(exc)) from exc
54
+
55
+ # Frozen source is copied, so future package changes cannot change this revision.
56
+ source = files("simple_module_cli").joinpath("migration_recipes", recipe).read_text()
57
+ revision = uuid4().hex[:12]
58
+ branch_labels = None if label in existing else (label,)
59
+ identifiers = (
60
+ f"\nrevision = {revision!r}\ndown_revision = {parent.revision!r}\n"
61
+ f"branch_labels = {branch_labels!r}\ndepends_on = None\n"
62
+ )
63
+ path = Path(script.versions) / f"{revision}_{upgrade_id.replace('.', '_')}.py"
64
+ with path.open("x") as output:
65
+ output.write(source + identifiers)
66
+ typer.echo(f"Prepared {upgrade_id}: {path}. Review it, then run alembic upgrade heads.")
@@ -0,0 +1,189 @@
1
+ """``smpy package-update`` — bump simple_module_* deps to latest PyPI versions.
2
+
3
+ Walks the project's ``pyproject.toml`` (and any ``[tool.uv.workspace]`` members),
4
+ finds every dependency whose distribution name starts with ``simple_module_`` /
5
+ ``simple-module-``, queries PyPI for the latest non-yanked release, and points
6
+ the constraint at it. Includes optional dependencies and dependency groups.
7
+ Matching npm packages use the selected core release when published on npm.
8
+
9
+ The rewrite preserves the pin style you wrote: ``==0.0.32`` becomes
10
+ ``==0.0.33``, ``>=0.0.32`` becomes ``>=0.0.33``, and an upper bound that
11
+ excludes the latest release skips the dependency instead of being dropped.
12
+ ``--loosen`` restores the older behaviour of rewriting every constraint to
13
+ ``name>=<latest>``.
14
+
15
+ Dependencies whose ``[tool.uv.sources]`` entry points at a workspace member, a
16
+ local path, a git ref, or a URL are left untouched — those aren't installed
17
+ from PyPI.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from concurrent.futures import ThreadPoolExecutor
23
+ from pathlib import Path
24
+ from typing import Annotated
25
+
26
+ import tomlkit
27
+ import typer
28
+
29
+ from simple_module_cli.package_update_deps import (
30
+ Change,
31
+ Skip,
32
+ collect_sm_deps,
33
+ local_sources,
34
+ normalize,
35
+ process_file,
36
+ source_map,
37
+ workspace_member_dirs,
38
+ )
39
+ from simple_module_cli.package_update_npm import load_manifests, plan_npm
40
+ from simple_module_cli.pypi import Fetcher, default_fetcher, fetch_latest
41
+
42
+ __all__ = ["package_update", "run_update"]
43
+
44
+
45
+ _CORE = "simple-module-core"
46
+
47
+
48
+ def _npm_target(cache: dict[str, str | None], skips: list[Skip]) -> tuple[str | None, str | None]:
49
+ """The core version npm pins follow, or why there is none.
50
+
51
+ npm pins move in lockstep with the Python core pin, so when this run left
52
+ that pin alone (an upper bound, a local source) the npm pins stay too.
53
+ """
54
+ for skip in skips:
55
+ if normalize(skip.package) == _CORE:
56
+ return None, f"simple_module_core not updated ({skip.reason})"
57
+ latest = next((v for n, v in cache.items() if normalize(n) == _CORE), None)
58
+ return latest, None if latest else "simple_module_core not found on PyPI"
59
+
60
+
61
+ def _print_summary(changes: list[Change], skips: list[Skip], dry_run: bool) -> None:
62
+ if not changes and not skips:
63
+ typer.echo("No simple_module_* or @simple-module-py/* dependencies found.")
64
+ return
65
+ by_file: dict[Path, list[str]] = {}
66
+ for c in changes:
67
+ by_file.setdefault(c.file, []).append(f" {c.package}: {c.old} → {c.new}")
68
+ for s in skips:
69
+ by_file.setdefault(s.file, []).append(f" {s.package}: skipped ({s.reason})")
70
+ for file, lines in by_file.items():
71
+ typer.echo(f"\n{file}")
72
+ for line in lines:
73
+ typer.echo(line)
74
+ if changes:
75
+ verb = "Would update" if dry_run else "Updated"
76
+ file_count = len({c.file for c in changes})
77
+ typer.echo(f"\n{verb} {len(changes)} dependency(ies) across {file_count} file(s).")
78
+ if not dry_run:
79
+ if any(c.file.suffix == ".toml" for c in changes):
80
+ typer.echo("Run `uv sync` to apply Python changes.")
81
+ for directory in sorted(
82
+ {c.file.parent for c in changes if c.file.name == "package.json"}
83
+ ):
84
+ typer.echo(f"Run `npm install` in {directory} to apply npm changes.")
85
+ elif skips:
86
+ typer.echo("\nNo updates applied.")
87
+
88
+
89
+ def run_update(
90
+ path: Path,
91
+ *,
92
+ dry_run: bool = False,
93
+ include_pre: bool = False,
94
+ loosen: bool = False,
95
+ fetcher: Fetcher | None = None,
96
+ ) -> None:
97
+ """Programmatic entry point — separated from the Typer command for testing."""
98
+ path = path.resolve()
99
+ root = path if path.name == "pyproject.toml" else path / "pyproject.toml"
100
+ if not root.is_file():
101
+ typer.echo(f"ERROR: {root} not found.", err=True)
102
+ raise typer.Exit(code=1)
103
+
104
+ fetch = fetcher or default_fetcher
105
+ root_doc = tomlkit.parse(root.read_bytes().decode("utf-8"))
106
+ files: list[tuple[Path, tomlkit.TOMLDocument]] = [(root, root_doc)]
107
+ for member in workspace_member_dirs(root, root_doc):
108
+ if member == root:
109
+ continue
110
+ files.append((member, tomlkit.parse(member.read_bytes().decode("utf-8"))))
111
+
112
+ npm_manifests = load_manifests([file.parent for file, _ in files])
113
+ inherited = source_map(root_doc)
114
+ locals_by_file = {file: local_sources(doc, inherited) for file, doc in files}
115
+
116
+ # Pre-fetch all unique sm packages in parallel — PyPI calls dominate runtime.
117
+ names = {n for file, doc in files for n in collect_sm_deps(doc, locals_by_file[file])}
118
+ if any(manifest.packages for manifest in npm_manifests) and not any(
119
+ normalize(n) == _CORE for n in names
120
+ ):
121
+ names.add("simple_module_core")
122
+ unique_names = sorted(names)
123
+ cache: dict[str, str | None] = {}
124
+ if unique_names:
125
+ with ThreadPoolExecutor(max_workers=min(8, len(unique_names))) as pool:
126
+ results = pool.map(
127
+ lambda n: (n, fetch_latest(n, include_pre=include_pre, fetcher=fetch)),
128
+ unique_names,
129
+ )
130
+ cache = dict(results)
131
+
132
+ all_changes: list[Change] = []
133
+ all_skips: list[Skip] = []
134
+ pending: list[tuple[Path, str]] = []
135
+
136
+ for file, doc in files:
137
+ changes, skips, new_doc = process_file(
138
+ file, doc, cache=cache, loosen=loosen, local=locals_by_file[file]
139
+ )
140
+ all_changes.extend(changes)
141
+ all_skips.extend(skips)
142
+ if new_doc is not None:
143
+ pending.append((file, tomlkit.dumps(new_doc)))
144
+
145
+ target, blocked = _npm_target(cache, all_skips)
146
+ changes, skips, npm_pending = plan_npm(npm_manifests, target, fetch, blocked=blocked)
147
+ all_changes.extend(changes)
148
+ all_skips.extend(skips)
149
+ pending.extend(npm_pending)
150
+
151
+ if not dry_run:
152
+ for file, content in pending:
153
+ # newline="" keeps each file's own line endings (CRLF must not become CRCRLF).
154
+ file.write_text(content, encoding="utf-8", newline="")
155
+
156
+ _print_summary(all_changes, all_skips, dry_run)
157
+
158
+
159
+ def package_update(
160
+ path: Annotated[
161
+ Path,
162
+ typer.Option("--path", help="Project root or pyproject.toml. Defaults to cwd."),
163
+ ] = Path(),
164
+ dry_run: Annotated[
165
+ bool,
166
+ typer.Option("--dry-run", help="Show planned changes without writing."),
167
+ ] = False,
168
+ include_pre: Annotated[
169
+ bool,
170
+ typer.Option("--include-pre", help="Include pre-release versions."),
171
+ ] = False,
172
+ loosen: Annotated[
173
+ bool,
174
+ typer.Option(
175
+ "--loosen",
176
+ help="Rewrite Python constraints to `>=<latest>` instead of keeping their operator.",
177
+ ),
178
+ ] = False,
179
+ ) -> None:
180
+ """Update simple_module_* dependencies and lockstep @simple-module-py/* pins.
181
+
182
+ Each constraint keeps the operator it already has — `==0.0.32` becomes
183
+ `==0.0.33`, `>=0.0.32` becomes `>=0.0.33` — so bumping versions never
184
+ silently changes a project's pinning policy. A dependency whose upper
185
+ bound excludes the latest release is reported and left alone. Pass
186
+ `--loosen` to rewrite Python constraints to `>=<latest>` instead.
187
+ npm pins keep exact, caret or tilde style and skip unpublished core versions.
188
+ """
189
+ run_update(path, dry_run=dry_run, include_pre=include_pre, loosen=loosen)
@@ -0,0 +1,153 @@
1
+ """Python manifest discovery and dependency planning for package-update."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import tomlkit
11
+ from tomlkit.items import Array
12
+
13
+ from simple_module_cli.requirements import parse_requirement, rewrite_requirement
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Change:
18
+ file: Path
19
+ package: str
20
+ old: str
21
+ new: str
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Skip:
26
+ file: Path
27
+ package: str
28
+ reason: str
29
+
30
+
31
+ def normalize(name: str) -> str:
32
+ return re.sub(r"[-_.]+", "-", name).lower()
33
+
34
+
35
+ def _is_sm_package(name: str) -> bool:
36
+ return normalize(name).startswith("simple-module-")
37
+
38
+
39
+ def _dep_name(spec: str) -> str | None:
40
+ """Extract the distribution name from a PEP 508 requirement string."""
41
+ parsed = parse_requirement(spec)
42
+ return parsed.name if parsed else None
43
+
44
+
45
+ def _is_local_source(source: Any) -> bool:
46
+ if isinstance(source, (list, Array)):
47
+ return any(_is_local_source(item) for item in source)
48
+ if not isinstance(source, dict):
49
+ return False
50
+ if source.get("workspace") is True:
51
+ return True
52
+ return any(key in source for key in ("path", "git", "url"))
53
+
54
+
55
+ def _get_uv_section(doc: tomlkit.TOMLDocument, key: str) -> dict[str, Any] | None:
56
+ tool = doc.get("tool")
57
+ if not isinstance(tool, dict):
58
+ return None
59
+ uv = tool.get("uv")
60
+ if not isinstance(uv, dict):
61
+ return None
62
+ section = uv.get(key)
63
+ return section if isinstance(section, dict) else None
64
+
65
+
66
+ def workspace_member_dirs(root_pyproject: Path, doc: tomlkit.TOMLDocument) -> list[Path]:
67
+ workspace = _get_uv_section(doc, "workspace")
68
+ if workspace is None:
69
+ return []
70
+ members = workspace.get("members") or []
71
+ base = root_pyproject.parent
72
+ out: list[Path] = []
73
+ for pattern in members:
74
+ for match in sorted(base.glob(str(pattern))):
75
+ if (match / "pyproject.toml").is_file():
76
+ out.append(match / "pyproject.toml")
77
+ return list(dict.fromkeys(p.resolve() for p in out))
78
+
79
+
80
+ def source_map(doc: tomlkit.TOMLDocument) -> dict[str, Any]:
81
+ return {normalize(name): src for name, src in (_get_uv_section(doc, "sources") or {}).items()}
82
+
83
+
84
+ def local_sources(doc: tomlkit.TOMLDocument, inherited: dict[str, Any]) -> set[str]:
85
+ sources = {**inherited, **source_map(doc)}
86
+ return {name for name, source in sources.items() if _is_local_source(source)}
87
+
88
+
89
+ def _dependency_arrays(doc: tomlkit.TOMLDocument):
90
+ project = doc.get("project", {})
91
+ if isinstance(project, dict):
92
+ deps = project.get("dependencies")
93
+ if isinstance(deps, (list, Array)):
94
+ yield deps
95
+ optional = project.get("optional-dependencies", {})
96
+ if isinstance(optional, dict):
97
+ for deps in optional.values():
98
+ if isinstance(deps, (list, Array)):
99
+ yield deps
100
+ groups = doc.get("dependency-groups", {})
101
+ if isinstance(groups, dict):
102
+ for deps in groups.values():
103
+ if isinstance(deps, (list, Array)):
104
+ yield deps
105
+
106
+
107
+ def collect_sm_deps(doc: tomlkit.TOMLDocument, local: set[str]) -> list[str]:
108
+ return [
109
+ name
110
+ for deps in _dependency_arrays(doc)
111
+ for raw in deps
112
+ if isinstance(raw, str)
113
+ and (name := _dep_name(raw))
114
+ and _is_sm_package(name)
115
+ and normalize(name) not in local
116
+ ]
117
+
118
+
119
+ def process_file(
120
+ path: Path,
121
+ doc: tomlkit.TOMLDocument,
122
+ *,
123
+ cache: dict[str, str | None],
124
+ loosen: bool = False,
125
+ local: set[str],
126
+ ) -> tuple[list[Change], list[Skip], tomlkit.TOMLDocument | None]:
127
+ changes: list[Change] = []
128
+ skips: list[Skip] = []
129
+
130
+ for deps in _dependency_arrays(doc):
131
+ for idx, raw in enumerate(deps):
132
+ if not isinstance(raw, str):
133
+ continue
134
+ dep_str = raw
135
+ name = _dep_name(dep_str)
136
+ if not name or not _is_sm_package(name):
137
+ continue
138
+ if normalize(name) in local:
139
+ skips.append(Skip(path, name, "workspace/local source"))
140
+ continue
141
+ latest = cache.get(name)
142
+ if latest is None:
143
+ skips.append(Skip(path, name, "not found on PyPI"))
144
+ continue
145
+ result = rewrite_requirement(dep_str, latest, loosen=loosen)
146
+ if result.spec is None:
147
+ if result.reason:
148
+ skips.append(Skip(path, name, result.reason))
149
+ continue
150
+ deps[idx] = result.spec
151
+ changes.append(Change(path, name, dep_str.strip(), result.spec))
152
+
153
+ return changes, skips, doc if changes else None
@@ -0,0 +1,126 @@
1
+ """Plan lockstep npm updates without installing packages or writing files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from urllib.parse import quote
11
+
12
+ from simple_module_cli.package_update_deps import Change, Skip
13
+ from simple_module_cli.pypi import Fetcher
14
+
15
+ _SECTIONS = ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies")
16
+ _PREFIX = "@simple-module-py/"
17
+ _REGISTRY = "https://registry.npmjs.org/"
18
+ _SEMVER = re.compile(
19
+ r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
20
+ r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
21
+ r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
22
+ )
23
+
24
+
25
+ def _version_key(version: str) -> tuple | None:
26
+ match = _SEMVER.fullmatch(version)
27
+ if not match:
28
+ return None
29
+ major, minor, patch, pre = match.groups()
30
+ identifiers = (pre or "").split(".") if pre else []
31
+ if any(part.isdigit() and len(part) > 1 and part.startswith("0") for part in identifiers):
32
+ return None
33
+ suffix = tuple((0, int(p)) if p.isdigit() else (1, p) for p in identifiers)
34
+ return (int(major), int(minor), int(patch), pre is None, suffix)
35
+
36
+
37
+ @dataclass
38
+ class Manifest:
39
+ path: Path
40
+ original: str
41
+ doc: dict[str, Any]
42
+
43
+ @property
44
+ def packages(self) -> set[str]:
45
+ return {
46
+ name
47
+ for section in _SECTIONS
48
+ for name in self.doc.get(section, {})
49
+ if name.startswith(_PREFIX)
50
+ }
51
+
52
+
53
+ def load_manifests(directories: list[Path]) -> list[Manifest]:
54
+ manifests = []
55
+ paths = dict.fromkeys(
56
+ path.resolve()
57
+ for directory in directories
58
+ for path in (directory / "package.json", directory / "client_app/package.json")
59
+ if path.is_file()
60
+ )
61
+ for path in paths:
62
+ original = path.read_bytes().decode("utf-8")
63
+ doc = json.loads(original)
64
+ if not isinstance(doc, dict):
65
+ raise ValueError(f"{path}: expected a JSON object")
66
+ for section in _SECTIONS:
67
+ if section in doc and not isinstance(doc[section], dict):
68
+ raise ValueError(f"{path}: {section} must be an object")
69
+ manifests.append(Manifest(path, original, doc))
70
+ return manifests
71
+
72
+
73
+ def _render(manifest: Manifest) -> str:
74
+ match = re.search(r"(?:\r?\n)([ \t]+)\S", manifest.original)
75
+ indent = match[1] if match else None
76
+ newline = "\r\n" if "\r\n" in manifest.original else "\n"
77
+ rendered = json.dumps(manifest.doc, indent=indent, ensure_ascii=False)
78
+ if newline != "\n":
79
+ rendered = rendered.replace("\n", newline)
80
+ return rendered + (newline if manifest.original.endswith("\n") else "")
81
+
82
+
83
+ def plan_npm(
84
+ manifests: list[Manifest], target: str | None, fetcher: Fetcher, *, blocked: str | None = None
85
+ ) -> tuple[list[Change], list[Skip], list[tuple[Path, str]]]:
86
+ changes: list[Change] = []
87
+ skips: list[Skip] = []
88
+ pending: list[tuple[Path, str]] = []
89
+ available: dict[str, bool] = {}
90
+ target_key = _version_key(target) if target else None
91
+ for manifest in manifests:
92
+ changed = False
93
+ for section in _SECTIONS:
94
+ for name, pin in manifest.doc.get(section, {}).items():
95
+ if not name.startswith(_PREFIX):
96
+ continue
97
+ style = pin[:1] if isinstance(pin, str) and pin[:1] in ("^", "~") else ""
98
+ current = _version_key(pin[len(style) :]) if isinstance(pin, str) else None
99
+ reason = None
100
+ if current is None:
101
+ reason = f"unsupported npm reference {pin!r}"
102
+ elif target_key is None:
103
+ reason = blocked or f"no npm-compatible simple_module_core target ({target})"
104
+ elif current >= target_key:
105
+ continue
106
+ else:
107
+ if name not in available:
108
+ try:
109
+ data = fetcher(_REGISTRY + quote(name, safe="@"))
110
+ available[name] = isinstance(data, dict) and target in (
111
+ data.get("versions") or {}
112
+ )
113
+ except (OSError, ValueError): # HTTP/URL/timeout, bad JSON
114
+ available[name] = False
115
+ if not available[name]:
116
+ reason = f"npm release {target} unavailable"
117
+ if reason:
118
+ skips.append(Skip(manifest.path, name, reason))
119
+ continue
120
+ new = style + str(target)
121
+ manifest.doc[section][name] = new
122
+ changes.append(Change(manifest.path, name, pin, new))
123
+ changed = True
124
+ if changed:
125
+ pending.append((manifest.path, _render(manifest)))
126
+ return changes, skips, pending