framework-m-core 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/.gitignore +3 -0
  2. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/CHANGELOG.md +18 -0
  3. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/PKG-INFO +1 -1
  4. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/pyproject.toml +1 -1
  5. framework_m_core-0.5.0/src/framework_m_core/cli/init.py +137 -0
  6. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/cli/main.py +4 -0
  7. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/cli/plugin_loader.py +5 -0
  8. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/domain/outbox.py +2 -2
  9. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/pii.py +3 -3
  10. framework_m_core-0.5.0/tests/cli/test_cli_commands.py +301 -0
  11. framework_m_core-0.5.0/tests/cli/test_init.py +150 -0
  12. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/README.md +0 -0
  13. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/__init__.py +0 -0
  14. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/cli/__init__.py +0 -0
  15. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/cli/config.py +0 -0
  16. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/cli/utility.py +0 -0
  17. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/config.py +0 -0
  18. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/container.py +0 -0
  19. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/decorators.py +0 -0
  20. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/__init__.py +0 -0
  21. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/activity_log.py +0 -0
  22. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/api_key.py +0 -0
  23. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/custom_permission.py +0 -0
  24. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/document_share.py +0 -0
  25. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/email_queue.py +0 -0
  26. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/error_log.py +0 -0
  27. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/file.py +0 -0
  28. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/job_log.py +0 -0
  29. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/notification.py +0 -0
  30. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/print_format.py +0 -0
  31. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/recent_document.py +0 -0
  32. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/report.py +0 -0
  33. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/scheduled_job.py +0 -0
  34. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/session.py +0 -0
  35. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/social_account.py +0 -0
  36. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/system_settings.py +0 -0
  37. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/tenant_translation.py +0 -0
  38. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/todo.py +0 -0
  39. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/translation.py +0 -0
  40. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/user.py +0 -0
  41. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/webhook.py +0 -0
  42. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/webhook_log.py +0 -0
  43. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/workflow.py +0 -0
  44. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/workflow_state.py +0 -0
  45. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/doctypes/workflow_transition.py +0 -0
  46. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/domain/__init__.py +0 -0
  47. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/domain/base_controller.py +0 -0
  48. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/domain/base_doctype.py +0 -0
  49. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/domain/mixins.py +0 -0
  50. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/domain/naming_counter.py +0 -0
  51. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/events/__init__.py +0 -0
  52. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/exceptions.py +0 -0
  53. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/__init__.py +0 -0
  54. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/audit.py +0 -0
  55. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/auth_context.py +0 -0
  56. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/authentication.py +0 -0
  57. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/base_doctype.py +0 -0
  58. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/bootstrap.py +0 -0
  59. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/cache.py +0 -0
  60. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/controller.py +0 -0
  61. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/email_queue.py +0 -0
  62. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/email_sender.py +0 -0
  63. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/event_bus.py +0 -0
  64. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/i18n.py +0 -0
  65. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/identity.py +0 -0
  66. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/job_queue.py +0 -0
  67. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/notification.py +0 -0
  68. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/oauth.py +0 -0
  69. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/permission.py +0 -0
  70. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/print.py +0 -0
  71. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/read_model.py +0 -0
  72. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/report_engine.py +0 -0
  73. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/repository.py +0 -0
  74. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/schema_mapper.py +0 -0
  75. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/search.py +0 -0
  76. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/session.py +0 -0
  77. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/socket.py +0 -0
  78. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/storage.py +0 -0
  79. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/tenant.py +0 -0
  80. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/interfaces/workflow.py +0 -0
  81. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/permission_lookup.py +0 -0
  82. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/permissions.py +0 -0
  83. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/py.typed +0 -0
  84. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/registry.py +0 -0
  85. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/rls.py +0 -0
  86. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/rpc_registry.py +0 -0
  87. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/security.py +0 -0
  88. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/services/__init__.py +0 -0
  89. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/services/user_manager.py +0 -0
  90. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/system_context.py +0 -0
  91. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/types/job_context.py +0 -0
  92. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/src/framework_m_core/unit_of_work.py +0 -0
  93. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/cli/test_cli_main.py +0 -0
  94. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/cli/test_config.py +0 -0
  95. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/cli/test_plugin_loader.py +0 -0
  96. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/cli/test_utility.py +0 -0
  97. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/conftest.py +0 -0
  98. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_api_key.py +0 -0
  99. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_job_log.py +0 -0
  100. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_report.py +0 -0
  101. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_scheduled_job.py +0 -0
  102. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_social_account.py +0 -0
  103. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_user.py +0 -0
  104. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_webhook.py +0 -0
  105. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/doctypes/test_webhook_log.py +0 -0
  106. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/events/test_doc_events.py +0 -0
  107. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/interfaces/test_cache.py +0 -0
  108. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/interfaces/test_email_queue_protocol.py +0 -0
  109. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/interfaces/test_identity.py +0 -0
  110. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/interfaces/test_socket.py +0 -0
  111. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/interfaces/test_workflow.py +0 -0
  112. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/services/test_user_manager.py +0 -0
  113. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_activity_log.py +0 -0
  114. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_child_table_permissions.py +0 -0
  115. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_container.py +0 -0
  116. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_custom_permission.py +0 -0
  117. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_custom_protocols.py +0 -0
  118. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_document_share.py +0 -0
  119. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_email_queue.py +0 -0
  120. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_error_log.py +0 -0
  121. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_exceptions.py +0 -0
  122. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_file.py +0 -0
  123. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_link_field_leakage.py +0 -0
  124. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_meta_registry_overrides.py +0 -0
  125. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_notification.py +0 -0
  126. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_object_level_permissions.py +0 -0
  127. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_outbox.py +0 -0
  128. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_permission_config.py +0 -0
  129. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_permission_conveniences.py +0 -0
  130. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_permission_lookup.py +0 -0
  131. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_pii.py +0 -0
  132. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_print_format.py +0 -0
  133. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_rls.py +0 -0
  134. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_rpc_decorator.py +0 -0
  135. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_schema_extension.py +0 -0
  136. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_session.py +0 -0
  137. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_share_lookup.py +0 -0
  138. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_system_context.py +0 -0
  139. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_system_settings.py +0 -0
  140. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_table_alteration.py +0 -0
  141. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_team_based_access.py +0 -0
  142. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_tenant.py +0 -0
  143. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_unit_of_work.py +0 -0
  144. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/test_whitelist_decorator.py +0 -0
  145. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/core/types/test_job_context.py +0 -0
  146. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/domain/test_base_controller.py +0 -0
  147. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/domain/test_base_doctype_implements_protocol.py +0 -0
  148. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/domain/test_mixins.py +0 -0
  149. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_audit.py +0 -0
  150. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_auth_context.py +0 -0
  151. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_base_doctype_protocol.py +0 -0
  152. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_bootstrap_protocol.py +0 -0
  153. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_controller_protocol.py +0 -0
  154. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_event_bus.py +0 -0
  155. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_i18n.py +0 -0
  156. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_job_queue.py +0 -0
  157. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_permission.py +0 -0
  158. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_print.py +0 -0
  159. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_repository.py +0 -0
  160. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_schema_mapper_protocol.py +0 -0
  161. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_search.py +0 -0
  162. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/interfaces/test_storage.py +0 -0
  163. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/core/test_registry.py +0 -0
  164. {framework_m_core-0.4.2 → framework_m_core-0.5.0}/tests/unit/test_base_doctype.py +0 -0
@@ -77,6 +77,9 @@ docker-compose.override.yml
77
77
  # Studio UI built assets
78
78
  apps/studio/src/framework_m_studio/static/
79
79
 
80
+ # Desk UI built assets (bundled with framework-m package)
81
+ libs/framework-m/src/framework_m/static/assets/
82
+
80
83
  #doctypes
81
84
  doctypes/*.py
82
85
  apps/studio/src/doctypes/*.py
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## framework-m-core v0.5.0
4
+
5
+ ### Features
6
+
7
+ - implement m start and m dev commands (4c80fc1)
8
+ - implement bundled Desk distribution with npm package (20b3be4)
9
+
10
+ ### Bug Fixes
11
+
12
+ - lint errors (9b6ab60)
13
+ - lint-import (8738781)
14
+ - switch tabs, doctype name spacing and foreign key field for doctypes (036f34c)
15
+ - Fix GitLab CI test failures (589d783)
16
+ - add framework-m-desk to pnpm-workspace.yaml (43deefa)
17
+ - lint format and import (9116aec)
18
+ - lint errors and rename publish-npm-package job to publish-npm-package-gitlab (77e2dce)
19
+
20
+
3
21
  ## framework-m-core v0.4.2
4
22
 
5
23
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: framework-m-core
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Core protocols and dependency injection for Framework M
5
5
  Author: Framework M Contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "framework-m-core"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "Core protocols and dependency injection for Framework M"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -0,0 +1,137 @@
1
+ """Init CLI Command - Scaffold frontend from template.
2
+
3
+ This module provides commands to initialize new components of Framework M.
4
+
5
+ Architecture:
6
+ - No global state - all paths passed explicitly
7
+ - Template resources via importlib.resources (Python 3.9+)
8
+ - Explicit error handling for file operations
9
+ - User feedback with print statements
10
+
11
+ Usage:
12
+ m init:frontend # Initialize in ./frontend
13
+ m init:frontend ../my-frontend # Initialize in custom location
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import shutil
19
+ import subprocess
20
+ from importlib.resources import files
21
+ from pathlib import Path
22
+ from typing import Annotated
23
+
24
+ import cyclopts
25
+
26
+
27
+ def _copy_template(source_template: Path, target: Path) -> None:
28
+ """Copy template directory to target location.
29
+
30
+ Args:
31
+ source_template: Source template directory
32
+ target: Target directory
33
+
34
+ Raises:
35
+ FileExistsError: If target already exists
36
+ RuntimeError: If copy fails
37
+ """
38
+ if target.exists():
39
+ msg = f"Target directory already exists: {target}"
40
+ raise FileExistsError(msg)
41
+
42
+ try:
43
+ shutil.copytree(source_template, target)
44
+ print(f"✓ Copied template to {target}")
45
+ except Exception as e:
46
+ msg = f"Failed to copy template: {e}"
47
+ raise RuntimeError(msg) from e
48
+
49
+
50
+ def _install_dependencies(target: Path) -> None:
51
+ """Install pnpm dependencies in target directory.
52
+
53
+ Args:
54
+ target: Frontend directory
55
+
56
+ Raises:
57
+ RuntimeError: If pnpm install fails
58
+ """
59
+ print("\nInstalling dependencies with pnpm...")
60
+ try:
61
+ subprocess.run(
62
+ ["pnpm", "install"],
63
+ cwd=str(target),
64
+ check=True,
65
+ )
66
+ print("✓ Dependencies installed")
67
+ except subprocess.CalledProcessError as e:
68
+ msg = f"Failed to install dependencies: {e}"
69
+ raise RuntimeError(msg) from e
70
+
71
+
72
+ def init_frontend_command(
73
+ target: Annotated[
74
+ Path | None,
75
+ cyclopts.Parameter(
76
+ name=["target"],
77
+ help="Target directory for frontend (default: ./frontend)",
78
+ ),
79
+ ] = None,
80
+ ) -> None:
81
+ """Initialize a new frontend from template.
82
+
83
+ This scaffolds the bundled Desk frontend template to a target directory
84
+ and installs dependencies. The template includes:
85
+ - React + TypeScript + Vite
86
+ - @framework-m/desk npm package integration
87
+ - Refine.dev setup with authProvider, dataProvider, liveProvider
88
+ - Tailwind CSS + shadcn/ui components
89
+
90
+ Examples:
91
+ m init:frontend # Initialize in ./frontend
92
+ m init:frontend ../my-frontend # Custom location
93
+
94
+ Architecture Notes:
95
+ - Uses apps/studio/frontend as template source
96
+ - No global state - all paths explicit
97
+ - Validates target doesn't exist (prevents overwrites)
98
+ - Runs pnpm install automatically
99
+ """
100
+ # Default to ./frontend
101
+ if target is None:
102
+ target = Path("frontend")
103
+
104
+ # Resolve to absolute path
105
+ target = target.resolve()
106
+
107
+ # Find template source from framework_m package
108
+ try:
109
+ template_resource = files("framework_m") / "templates" / "frontend"
110
+ if not template_resource.is_dir():
111
+ msg = "Frontend template not found in framework_m package"
112
+ raise RuntimeError(msg)
113
+
114
+ # Convert to Path for file operations
115
+ template_source = Path(str(template_resource))
116
+ except Exception as e:
117
+ msg = f"Failed to locate template: {e}"
118
+ raise RuntimeError(msg) from e
119
+
120
+ print(f"Initializing frontend at {target}")
121
+ print(f"Template source: {template_source}")
122
+
123
+ # Copy template
124
+ _copy_template(template_source, target)
125
+
126
+ # Install dependencies
127
+ _install_dependencies(target)
128
+
129
+ print("\n✓ Frontend initialized successfully!")
130
+ print("\nNext steps:")
131
+ print(f" cd {target}")
132
+ print(" pnpm dev")
133
+ print("\nOr start with:")
134
+ print(" m start --with-frontend")
135
+
136
+
137
+ __all__ = ["init_frontend_command"]
@@ -2,6 +2,7 @@ import cyclopts
2
2
 
3
3
  from framework_m_core import __version__
4
4
  from framework_m_core.cli.config import config_set_command, config_show_command
5
+ from framework_m_core.cli.init import init_frontend_command
5
6
  from framework_m_core.cli.plugin_loader import load_plugins
6
7
  from framework_m_core.cli.utility import info_command
7
8
 
@@ -18,6 +19,9 @@ app.command(info_command, name="info")
18
19
  app.command(config_show_command, name="config:show")
19
20
  app.command(config_set_command, name="config:set")
20
21
 
22
+ # Init commands
23
+ app.command(init_frontend_command, name="init:frontend")
24
+
21
25
  # Load plugins
22
26
  load_plugins(app)
23
27
 
@@ -94,6 +94,11 @@ def register_plugins(app: cyclopts.App, plugins: list[EntryPoint]) -> None:
94
94
  logger.warning(
95
95
  f"Plugin '{ep.name}' is not a cyclopts App or callable, skipping"
96
96
  )
97
+ except cyclopts.exceptions.CommandCollisionError:
98
+ # Command already registered (core takes precedence over plugins)
99
+ logger.debug(
100
+ f"CLI plugin '{ep.name}' skipped - command already registered by core"
101
+ )
97
102
  except Exception as e:
98
103
  logger.warning(
99
104
  f"Failed to register CLI plugin '{ep.name}': {e}",
@@ -27,7 +27,7 @@ Example:
27
27
  from __future__ import annotations
28
28
 
29
29
  from datetime import UTC, datetime
30
- from enum import Enum
30
+ from enum import StrEnum
31
31
  from typing import Any
32
32
  from uuid import UUID, uuid4
33
33
 
@@ -39,7 +39,7 @@ def _utc_now() -> datetime:
39
39
  return datetime.now(UTC)
40
40
 
41
41
 
42
- class OutboxStatus(str, Enum):
42
+ class OutboxStatus(StrEnum):
43
43
  """Status of an outbox entry.
44
44
 
45
45
  Attributes:
@@ -23,7 +23,7 @@ Example:
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- from enum import Enum
26
+ from enum import StrEnum
27
27
  from typing import Any
28
28
 
29
29
  from pydantic import BaseModel, Field
@@ -35,7 +35,7 @@ from framework_m_core.config import load_config
35
35
  # =============================================================================
36
36
 
37
37
 
38
- class AuthMode(str, Enum):
38
+ class AuthMode(StrEnum):
39
39
  """Available authentication modes.
40
40
 
41
41
  Attributes:
@@ -142,7 +142,7 @@ def is_sensitive_pii(field_name: str) -> bool:
142
142
  # =============================================================================
143
143
 
144
144
 
145
- class DeletionMode(str, Enum):
145
+ class DeletionMode(StrEnum):
146
146
  """How to handle user data on account deletion.
147
147
 
148
148
  Attributes:
@@ -0,0 +1,301 @@
1
+ """Fast CLI command tests.
2
+
3
+ Tests CLI commands without heavy npm operations.
4
+ Focus on command structure, error handling, and fast validations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+
15
+ class TestInitFrontendCommand:
16
+ """Test m init:frontend command."""
17
+
18
+ def test_command_available(self) -> None:
19
+ """Test that init:frontend command is registered."""
20
+ result = subprocess.run(
21
+ ["python", "-m", "framework_m_core.cli.main", "init:frontend", "--help"],
22
+ capture_output=True,
23
+ text=True,
24
+ )
25
+ assert result.returncode == 0
26
+ assert "init:frontend" in result.stdout.lower() or "Initialize" in result.stdout
27
+
28
+ def test_fails_without_template(self, tmp_path: Path) -> None:
29
+ """Test fails gracefully when template not found."""
30
+ # Test by trying to initialize with a non-existent template via import failure
31
+ # The init command will fail during dependency installation if pnpm can't find packages
32
+ # This is effectively tested by the error handling in test_raises_runtime_error_when_template_not_found
33
+ # Just verify the command exists and accepts arguments
34
+ result = subprocess.run(
35
+ ["python", "-m", "framework_m_core.cli.main", "init:frontend", "--help"],
36
+ capture_output=True,
37
+ text=True,
38
+ )
39
+ assert result.returncode == 0
40
+
41
+ def test_fails_if_target_exists(self, tmp_path: Path) -> None:
42
+ """Test fails when target directory already exists."""
43
+ target = tmp_path / "existing"
44
+ target.mkdir()
45
+
46
+ result = subprocess.run(
47
+ ["python", "-m", "framework_m_core.cli.main", "init:frontend", str(target)],
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=5,
51
+ )
52
+
53
+ assert result.returncode != 0
54
+ assert (
55
+ "already exists" in result.stderr.lower()
56
+ or "already exists" in result.stdout.lower()
57
+ )
58
+
59
+
60
+ class TestStartCommand:
61
+ """Test m start command."""
62
+
63
+ def test_command_available(self) -> None:
64
+ """Test that start command is registered."""
65
+ result = subprocess.run(
66
+ ["python", "-m", "framework_m_core.cli.main", "start", "--help"],
67
+ capture_output=True,
68
+ text=True,
69
+ )
70
+ assert result.returncode == 0
71
+ assert "start" in result.stdout.lower() or "Start" in result.stdout
72
+
73
+ def test_accepts_port_parameter(self) -> None:
74
+ """Test that --port parameter is accepted."""
75
+ result = subprocess.run(
76
+ ["python", "-m", "framework_m_core.cli.main", "start", "--help"],
77
+ capture_output=True,
78
+ text=True,
79
+ )
80
+ assert "--port" in result.stdout.lower()
81
+
82
+ def test_accepts_with_frontend_parameter(self) -> None:
83
+ """Test that --with-frontend parameter is accepted."""
84
+ result = subprocess.run(
85
+ ["python", "-m", "framework_m_core.cli.main", "start", "--help"],
86
+ capture_output=True,
87
+ text=True,
88
+ )
89
+ assert "--with-frontend" in result.stdout.lower()
90
+
91
+
92
+ class TestCLIStructure:
93
+ """Test overall CLI structure."""
94
+
95
+ def test_main_cli_runs(self) -> None:
96
+ """Test that main CLI runs without errors."""
97
+ result = subprocess.run(
98
+ ["python", "-m", "framework_m_core.cli.main", "--help"],
99
+ capture_output=True,
100
+ text=True,
101
+ timeout=5,
102
+ )
103
+ assert result.returncode == 0
104
+ assert "framework" in result.stdout.lower() or "usage" in result.stdout.lower()
105
+
106
+ def test_all_commands_listed(self) -> None:
107
+ """Test that key commands are listed in help."""
108
+ result = subprocess.run(
109
+ ["python", "-m", "framework_m_core.cli.main", "--help"],
110
+ capture_output=True,
111
+ text=True,
112
+ )
113
+
114
+ # Should list major commands
115
+ help_text = result.stdout.lower()
116
+ assert "start" in help_text
117
+ assert "init" in help_text or "frontend" in help_text
118
+
119
+
120
+ @pytest.mark.skipif(
121
+ not Path("frontend").exists(),
122
+ reason="Frontend template not available",
123
+ )
124
+ class TestTemplateStructure:
125
+ """Test frontend template structure (fast validation)."""
126
+
127
+ def test_template_has_package_json(self) -> None:
128
+ """Test template has package.json."""
129
+ template = Path("frontend")
130
+ assert (template / "package.json").exists()
131
+
132
+ def test_template_has_vite_config(self) -> None:
133
+ """Test template has vite.config.ts."""
134
+ template = Path("frontend")
135
+ assert (template / "vite.config.ts").exists()
136
+
137
+ def test_template_has_tsconfig(self) -> None:
138
+ """Test template has tsconfig.json."""
139
+ template = Path("frontend")
140
+ assert (template / "tsconfig.json").exists()
141
+
142
+ def test_template_has_src_directory(self) -> None:
143
+ """Test template has src directory with essential files."""
144
+ template = Path("frontend")
145
+ assert (template / "src").exists()
146
+ assert (template / "src" / "App.tsx").exists()
147
+ assert (template / "src" / "index.tsx").exists()
148
+
149
+ def test_template_has_npmrc(self) -> None:
150
+ """Test template has .npmrc for pnpm configuration."""
151
+ template = Path("frontend")
152
+ assert (template / ".npmrc").exists()
153
+
154
+ def test_package_json_has_required_deps(self) -> None:
155
+ """Test package.json has required dependencies."""
156
+ template = Path("frontend")
157
+ package_json = template / "package.json"
158
+ content = package_json.read_text()
159
+
160
+ # Should have Refine and React
161
+ assert "@refinedev/core" in content
162
+ assert "react" in content
163
+ assert "vite" in content
164
+
165
+
166
+ @pytest.mark.skipif(
167
+ not Path("libs/framework-m-desk").exists(),
168
+ reason="@framework-m/desk package not available",
169
+ )
170
+ class TestDeskPackageStructure:
171
+ """Test @framework-m/desk package structure (fast validation)."""
172
+
173
+ def test_package_has_package_json(self) -> None:
174
+ """Test package has package.json."""
175
+ package = Path("libs/framework-m-desk")
176
+ assert (package / "package.json").exists()
177
+
178
+ def test_package_has_src_directory(self) -> None:
179
+ """Test package has src directory."""
180
+ package = Path("libs/framework-m-desk")
181
+ assert (package / "src").exists()
182
+ assert (package / "src" / "index.ts").exists()
183
+
184
+ def test_package_exports_providers(self) -> None:
185
+ """Test index.ts exports required providers."""
186
+ package = Path("libs/framework-m-desk")
187
+ index_file = package / "src" / "index.ts"
188
+ content = index_file.read_text()
189
+
190
+ expected_exports = [
191
+ "frameworkMDataProvider",
192
+ "authProvider",
193
+ "liveProvider",
194
+ "API_URL",
195
+ ]
196
+
197
+ for export in expected_exports:
198
+ assert export in content, f"Missing export: {export}"
199
+
200
+ def test_package_json_has_correct_config(self) -> None:
201
+ """Test package.json has correct configuration."""
202
+ package = Path("libs/framework-m-desk")
203
+ package_json = package / "package.json"
204
+ content = package_json.read_text()
205
+
206
+ assert "@framework-m/desk" in content
207
+ assert "dist/index.js" in content
208
+ assert "peerDependencies" in content
209
+
210
+
211
+ class TestFrontendBuildConfig:
212
+ """Test frontend build configuration (no actual build)."""
213
+
214
+ @pytest.mark.skipif(
215
+ not Path("frontend").exists(),
216
+ reason="Frontend template not available",
217
+ )
218
+ def test_vite_config_exists_and_valid(self) -> None:
219
+ """Test vite config file exists and has basic structure."""
220
+ vite_config = Path("frontend/vite.config.ts")
221
+ assert vite_config.exists()
222
+
223
+ content = vite_config.read_text()
224
+ assert "defineConfig" in content
225
+ assert "plugins" in content
226
+
227
+ @pytest.mark.skipif(
228
+ not Path("frontend").exists(),
229
+ reason="Frontend template not available",
230
+ )
231
+ def test_tsconfig_exists_and_valid(self) -> None:
232
+ """Test TypeScript config exists and has basic structure."""
233
+ tsconfig = Path("frontend/tsconfig.json")
234
+ assert tsconfig.exists()
235
+
236
+ content = tsconfig.read_text()
237
+ assert "compilerOptions" in content
238
+
239
+ @pytest.mark.skipif(
240
+ not Path("frontend").exists(),
241
+ reason="Frontend template not available",
242
+ )
243
+ def test_npmrc_has_peer_dependency_config(self) -> None:
244
+ """Test .npmrc has peer dependency configuration."""
245
+ npmrc = Path("frontend/.npmrc")
246
+ if npmrc.exists():
247
+ content = npmrc.read_text()
248
+ # Should have peer dependency settings for compatibility
249
+ assert (
250
+ "legacy-peer-deps" in content.lower()
251
+ or "strict-peer-dependencies" in content.lower()
252
+ )
253
+
254
+
255
+ class TestPackageJsonOverrides:
256
+ """Test package.json configuration."""
257
+
258
+ def test_root_package_json_exists_and_valid(self) -> None:
259
+ """Test root package.json exists and has valid monorepo config."""
260
+ package_json = Path("package.json")
261
+ if package_json.exists():
262
+ content = package_json.read_text()
263
+ import json
264
+
265
+ data = json.loads(content)
266
+ # Should be a monorepo with pnpm workspace
267
+ assert data.get("private") is True
268
+ # Overrides are optional - only needed if dependency conflicts exist
269
+ # Just verify the file is valid JSON and has basic monorepo structure
270
+
271
+
272
+ class TestAutoFormTypescriptFix:
273
+ """Test AutoForm TypeScript fixes."""
274
+
275
+ @pytest.mark.skipif(
276
+ not Path("frontend/src/components/form/AutoForm.tsx").exists(),
277
+ reason="AutoForm component not available",
278
+ )
279
+ def test_autoform_has_validator_type_cast(self) -> None:
280
+ """Test AutoForm has ValidatorType cast to fix type error."""
281
+ autoform = Path("frontend/src/components/form/AutoForm.tsx")
282
+ content = autoform.read_text()
283
+
284
+ # Should import ValidatorType
285
+ assert "ValidatorType" in content
286
+
287
+ # Should have type cast
288
+ assert "as ValidatorType" in content
289
+
290
+ @pytest.mark.skipif(
291
+ not Path("frontend/src/components/form/AutoForm.tsx").exists(),
292
+ reason="AutoForm component not available",
293
+ )
294
+ def test_autoform_imports_rjsf_correctly(self) -> None:
295
+ """Test AutoForm imports from @rjsf packages."""
296
+ autoform = Path("frontend/src/components/form/AutoForm.tsx")
297
+ content = autoform.read_text()
298
+
299
+ assert "@rjsf/core" in content
300
+ assert "@rjsf/utils" in content
301
+ assert "@rjsf/validator-ajv8" in content