datex-studio-cli 0.4.18__tar.gz → 0.4.19__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 (215) hide show
  1. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/PKG-INFO +1 -1
  2. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/pyproject.toml +1 -1
  3. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/__init__.py +1 -1
  4. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/auth.py +188 -31
  5. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/branch.py +202 -35
  6. datex_studio_cli-0.4.19/src/dxs/commands/branch_commit_guard.py +177 -0
  7. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/cascade.py +16 -37
  8. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/configuration.py +88 -1
  9. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/crm.py +404 -276
  10. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/crm_delivery.py +70 -20
  11. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/crm_time.py +91 -16
  12. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/explore.py +82 -2
  13. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/source.py +71 -5
  14. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/auth/decorators.py +25 -29
  15. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/auth/token_cache.py +82 -13
  16. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/generator.py +5 -1
  17. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/models.py +42 -0
  18. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/parsers.py +8 -1
  19. datex_studio_cli-0.4.19/src/dxs/core/dynamics/pagination.py +159 -0
  20. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/output/redaction.py +72 -5
  21. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/errors.py +42 -0
  22. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/resolvers.py +88 -1
  23. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/.gitignore +0 -0
  24. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/CONTEXT.md +0 -0
  25. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/README.md +0 -0
  26. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/.env.example +0 -0
  27. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/Dockerfile +0 -0
  28. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/README.md +0 -0
  29. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/apply-local-cache.js +0 -0
  30. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/collector-config.yaml +0 -0
  31. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/docker-compose.yml +0 -0
  32. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/local-test-settings.json +0 -0
  33. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/otel/remote-settings.json +0 -0
  34. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/report-creator.skill +0 -0
  35. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/scripts/analyze_corpus.py +0 -0
  36. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/scripts/generate_rdlx_schema.py +0 -0
  37. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/__main__.py +0 -0
  38. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/cli.py +0 -0
  39. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/__init__.py +0 -0
  40. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/api.py +0 -0
  41. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/config.py +0 -0
  42. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/datasource.py +0 -0
  43. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/devops.py +0 -0
  44. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/document.py +0 -0
  45. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/endpoint.py +0 -0
  46. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/env.py +0 -0
  47. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/function.py +0 -0
  48. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/init.py +0 -0
  49. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/marketplace.py +0 -0
  50. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/__init__.py +0 -0
  51. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/_common.py +0 -0
  52. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/api.py +0 -0
  53. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/create.py +0 -0
  54. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/data.py +0 -0
  55. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/pack.py +0 -0
  56. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/ng/preview.py +0 -0
  57. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/odata.py +0 -0
  58. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/organization.py +0 -0
  59. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/proxy.py +0 -0
  60. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/reference.py +0 -0
  61. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/release_notes.py +0 -0
  62. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/repo.py +0 -0
  63. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/__init__.py +0 -0
  64. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/add_cmds.py +0 -0
  65. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/api.py +0 -0
  66. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/batch.py +0 -0
  67. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/create.py +0 -0
  68. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/data.py +0 -0
  69. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/dataset.py +0 -0
  70. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/edit.py +0 -0
  71. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/folder.py +0 -0
  72. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/preview.py +0 -0
  73. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/report/schema_cmds.py +0 -0
  74. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/reset.py +0 -0
  75. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/schema.py +0 -0
  76. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/servicepack.py +0 -0
  77. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/studio.py +0 -0
  78. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/telemetry.py +0 -0
  79. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/user.py +0 -0
  80. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/commands/validation_report.py +0 -0
  81. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/context.py +0 -0
  82. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/__init__.py +0 -0
  83. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/agent_browser.py +0 -0
  84. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/__init__.py +0 -0
  85. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/app_config.py +0 -0
  86. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/client.py +0 -0
  87. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/endpoints.py +0 -0
  88. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/metadata_models.py +0 -0
  89. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/models.py +0 -0
  90. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/api/reference_graph.py +0 -0
  91. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/auth/__init__.py +0 -0
  92. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/auth/datex_tenant.py +0 -0
  93. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/auth/msal_client.py +0 -0
  94. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/auth/tenants.py +0 -0
  95. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/cache.py +0 -0
  96. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/__init__.py +0 -0
  97. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/flow_generator.py +0 -0
  98. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/flow_models.py +0 -0
  99. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/flow_shape.py +0 -0
  100. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/resolver.py +0 -0
  101. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/type_def_parser.py +0 -0
  102. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/datasource/validator.py +0 -0
  103. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/designer/__init__.py +0 -0
  104. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/designer/flow_models.py +0 -0
  105. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/designer/models.py +0 -0
  106. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/designer/parsers.py +0 -0
  107. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/devops/__init__.py +0 -0
  108. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/devops/client.py +0 -0
  109. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/devops/helpers.py +0 -0
  110. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/devops/models.py +0 -0
  111. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/dynamics/__init__.py +0 -0
  112. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/dynamics/client.py +0 -0
  113. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/dynamics/html.py +0 -0
  114. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  115. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/dynamics/resolvers.py +0 -0
  116. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/dynamics/time_entry.py +0 -0
  117. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/footprint/__init__.py +0 -0
  118. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/footprint/client.py +0 -0
  119. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/footprint/edmx_parser.py +0 -0
  120. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/footprint/metadata.py +0 -0
  121. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/function/__init__.py +0 -0
  122. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/function/generator.py +0 -0
  123. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/function/models.py +0 -0
  124. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/function/validator.py +0 -0
  125. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/graph.py +0 -0
  126. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/onboarding.py +0 -0
  127. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/output/__init__.py +0 -0
  128. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/output/csv_fmt.py +0 -0
  129. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/output/formatter.py +0 -0
  130. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/output/json_fmt.py +0 -0
  131. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/output/yaml_fmt.py +0 -0
  132. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/release_notes.py +0 -0
  133. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/responses.py +0 -0
  134. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/telemetry/__init__.py +0 -0
  135. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/telemetry/config.py +0 -0
  136. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/telemetry/identity.py +0 -0
  137. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/telemetry/recorder.py +0 -0
  138. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/telemetry/scrubber.py +0 -0
  139. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/core/telemetry/sender.py +0 -0
  140. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/models/__init__.py +0 -0
  141. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/__init__.py +0 -0
  142. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/folder.py +0 -0
  143. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/manifest.py +0 -0
  144. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/mocks.py +0 -0
  145. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/preview.py +0 -0
  146. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/scaffold.py +0 -0
  147. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/ng/serve_lock.py +0 -0
  148. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/py.typed +0 -0
  149. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/__init__.py +0 -0
  150. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/datasource_binding.py +0 -0
  151. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/engine.py +0 -0
  152. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/field_parser.py +0 -0
  153. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/folder.py +0 -0
  154. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/manifest.py +0 -0
  155. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/models.py +0 -0
  156. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/preview.py +0 -0
  157. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/preview_arjs.py +0 -0
  158. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/schema.py +0 -0
  159. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/svg_renderer.py +0 -0
  160. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/templates/__init__.py +0 -0
  161. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  162. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  163. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/validate_arjs.py +0 -0
  164. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/validator.py +0 -0
  165. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/report/wrapper.py +0 -0
  166. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/__init__.py +0 -0
  167. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/click_options.py +0 -0
  168. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/clipboard.py +0 -0
  169. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/config.py +0 -0
  170. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/filtering.py +0 -0
  171. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/image.py +0 -0
  172. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/paths.py +0 -0
  173. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/responses.py +0 -0
  174. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/restricted.py +0 -0
  175. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/sorting.py +0 -0
  176. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/update_check.py +0 -0
  177. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/utils/versioning.py +0 -0
  178. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/__init__.py +0 -0
  179. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/app.py +0 -0
  180. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/design.py +0 -0
  181. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/proxy_app.py +0 -0
  182. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/404/index.html +0 -0
  183. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/404.html +0 -0
  184. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
  185. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
  186. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  187. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  188. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  189. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
  190. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  191. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  192. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  193. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  194. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  195. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
  196. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
  197. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
  198. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
  199. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  200. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  201. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  202. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  203. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  204. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  205. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  206. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  207. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/design/capture/index.html +0 -0
  208. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/design/capture/index.txt +0 -0
  209. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/design/index.html +0 -0
  210. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/design/index.txt +0 -0
  211. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/index.html +0 -0
  212. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/index.txt +0 -0
  213. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/render-cli.js +0 -0
  214. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/report-preview.html +0 -0
  215. {datex_studio_cli-0.4.18 → datex_studio_cli-0.4.19}/src/dxs/web/static/report-validate.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datex-studio-cli
3
- Version: 0.4.18
3
+ Version: 0.4.19
4
4
  Summary: CLI for Datex Studio low-code platform, designed for LLM-based AI agents
5
5
  Project-URL: Homepage, https://github.com/datex/datex-studio-cli
6
6
  Project-URL: Documentation, https://github.com/datex/datex-studio-cli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datex-studio-cli"
7
- version = "0.4.18"
7
+ version = "0.4.19"
8
8
  description = "CLI for Datex Studio low-code platform, designed for LLM-based AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
1
  """Datex Studio CLI - Command-line interface for Datex Studio platform."""
2
2
 
3
- __version__ = "0.4.18"
3
+ __version__ = "0.4.19"
4
4
  __app_name__ = "dxs"
@@ -16,6 +16,7 @@ from dxs.core.auth.datex_tenant import (
16
16
  )
17
17
  from dxs.core.auth.decorators import get_access_token
18
18
  from dxs.core.auth.tenants import (
19
+ identity_needs_tenant_authority,
19
20
  is_foreign_tenant,
20
21
  is_home_tenant,
21
22
  org_needs_tenant_login,
@@ -148,9 +149,12 @@ def run_standard_device_login(
148
149
  external_entra_id_domain_name=domain if external else None,
149
150
  )
150
151
  # Capture the stored key NOW; update_identity_org_info must not change the key
151
- # out from under us, and the computed key depends on org name (unknown until
152
- # discovery on the external-discover path).
153
- identity_key = saved_identity.identity_key
152
+ # out from under us, and the computed key changes once discovery fills in the
153
+ # org name on the external-discover path. Resolve rather than recompute: a
154
+ # legacy bare-keyed record for the same account+tenant is updated in place,
155
+ # so the stored key can be the bare username while the computed key is
156
+ # username#tenant.
157
+ identity_key = cache.resolve_stored_key(saved_identity) or saved_identity.identity_key
154
158
 
155
159
  if organization_id is None:
156
160
  org_info = _fetch_organization_info(ctx, base_url=api_base_url)
@@ -487,8 +491,29 @@ def logout(ctx: DxsContext, identity: str | None, logout_all: bool) -> None:
487
491
  return
488
492
 
489
493
  if identity:
490
- # Logout specific identity
494
+ # Logout specific identity. Resolve a bare username to its storage key
495
+ # (external identities live under username#org / username#tenant_id);
496
+ # several matches means the selector is ambiguous — never guess which
497
+ # identity to remove.
498
+ storage_key = identity
491
499
  existing = cache.get_identity(identity)
500
+ if not existing:
501
+ matches = cache.find_identity_keys_by_username(identity)
502
+ if len(matches) > 1:
503
+ ctx.output_error(
504
+ AuthenticationError(
505
+ message=f"'{identity}' matches more than one identity.",
506
+ code="DXS-AUTH-011",
507
+ suggestions=[
508
+ "Pass one of these exact identities to 'dxs auth logout':",
509
+ *matches,
510
+ ],
511
+ )
512
+ )
513
+ raise SystemExit(1)
514
+ if matches:
515
+ storage_key = matches[0]
516
+ existing = cache.get_identity(storage_key)
492
517
  if not existing:
493
518
  ctx.output(
494
519
  single(
@@ -503,7 +528,7 @@ def logout(ctx: DxsContext, identity: str | None, logout_all: bool) -> None:
503
528
  return
504
529
 
505
530
  was_active = cache.get_active_identity() == existing
506
- cache.remove_identity(identity)
531
+ cache.remove_identity(storage_key)
507
532
 
508
533
  remaining = cache.list_identities()
509
534
  ctx.output(
@@ -538,7 +563,9 @@ def logout(ctx: DxsContext, identity: str | None, logout_all: bool) -> None:
538
563
  return
539
564
 
540
565
  account_name = active.account_username
541
- cache.remove_identity(account_name or "")
566
+ # Remove by the real storage key — external identities are not stored
567
+ # under the bare username.
568
+ cache.remove_identity(cache.resolve_stored_key(active) or account_name or "")
542
569
 
543
570
  remaining = cache.list_identities()
544
571
  ctx.output(
@@ -1230,8 +1257,30 @@ def _switch_by_username(
1230
1257
  """Switch to an identity by username (original behavior)."""
1231
1258
  cache = MultiIdentityTokenCache()
1232
1259
 
1233
- # Check if identity exists
1260
+ # Check if identity exists. External identities are stored under
1261
+ # disambiguated keys (username#org, username#tenant_id), so a bare
1262
+ # username that is not itself a storage key still resolves when it
1263
+ # matches exactly one identity. Matching several is the same-UPN
1264
+ # multi-tenant case: refuse to guess and list the candidates.
1265
+ storage_key = username
1234
1266
  existing = cache.get_identity(username)
1267
+ if not existing:
1268
+ matches = cache.find_identity_keys_by_username(username)
1269
+ if len(matches) > 1:
1270
+ ctx.output_error(
1271
+ AuthenticationError(
1272
+ message=f"'{username}' matches more than one identity.",
1273
+ code="DXS-AUTH-011",
1274
+ suggestions=[
1275
+ "Pass one of these exact identities to 'dxs auth switch':",
1276
+ *matches,
1277
+ ],
1278
+ )
1279
+ )
1280
+ raise SystemExit(1)
1281
+ if matches:
1282
+ storage_key = matches[0]
1283
+ existing = cache.get_identity(storage_key)
1235
1284
  if not existing:
1236
1285
  # List available identities for suggestion
1237
1286
  available = cache.list_identities()
@@ -1254,7 +1303,7 @@ def _switch_by_username(
1254
1303
  # Check if token is expired
1255
1304
  if existing.is_expired():
1256
1305
  # Try to refresh
1257
- refreshed = cache.try_refresh_identity(username)
1306
+ refreshed = cache.try_refresh_identity(storage_key)
1258
1307
  if not refreshed:
1259
1308
  ctx.output_error(
1260
1309
  AuthenticationError(
@@ -1271,7 +1320,7 @@ def _switch_by_username(
1271
1320
 
1272
1321
  # Switch to the new identity
1273
1322
  try:
1274
- new_identity = cache.set_active_identity(username)
1323
+ new_identity = cache.set_active_identity(storage_key)
1275
1324
  except KeyError:
1276
1325
  ctx.output_error(
1277
1326
  AuthenticationError(
@@ -1283,7 +1332,7 @@ def _switch_by_username(
1283
1332
 
1284
1333
  # Persist daemon credentials on the identity if supplied
1285
1334
  if client_id and client_secret:
1286
- updated = cache.update_identity_daemon_creds(username, client_id, client_secret)
1335
+ updated = cache.update_identity_daemon_creds(storage_key, client_id, client_secret)
1287
1336
  if updated is not None:
1288
1337
  new_identity = updated
1289
1338
 
@@ -1291,7 +1340,7 @@ def _switch_by_username(
1291
1340
  "status": "switched",
1292
1341
  "message": f"Switched to {username}",
1293
1342
  "previous_identity": previous_username,
1294
- "active_identity": username,
1343
+ "active_identity": storage_key,
1295
1344
  "organization": new_identity.organization_name,
1296
1345
  "organization_id": new_identity.organization_id,
1297
1346
  }
@@ -1343,19 +1392,30 @@ def _switch_by_organization(
1343
1392
 
1344
1393
  # Step 3: Check whether this org lives in a tenant of its own
1345
1394
  if not org_needs_tenant_login(org_info):
1346
- # Home-tenant org (Datex): the current identity already covers it.
1347
- ctx.output(
1348
- single(
1349
- item={
1350
- "status": "no_action_required",
1351
- "message": f"Organization '{org_name}' authenticates against the Datex home tenant",
1352
- "organization": org_info.name,
1353
- "organization_id": org_info.id,
1354
- "suggestion": "Your current identity should work with this organization",
1355
- },
1356
- semantic_key="authentication",
1395
+ # No device login is needed — but "no login" is not "no action".
1396
+ if org_info.external_entra_id:
1397
+ # Flagged external yet resolving to no foreign tenant: a
1398
+ # misregistered org record must never capture the caller's
1399
+ # identity (see _handled_home_tenant_external_org).
1400
+ if _handled_home_tenant_external_org(ctx, org_info):
1401
+ return
1402
+ ctx.output_error(
1403
+ AuthenticationError(
1404
+ message=(
1405
+ f"Organization '{org_name}' requires external authentication "
1406
+ "but has no tenant ID configured"
1407
+ ),
1408
+ code="DXS-AUTH-012",
1409
+ suggestions=[
1410
+ "Contact your organization administrator to configure the tenant ID"
1411
+ ],
1412
+ )
1357
1413
  )
1358
- )
1414
+ raise SystemExit(1)
1415
+ # Home-tenant org (Datex, or an org with no tenant of its own): the
1416
+ # current identity's tokens already cover it, but the org binding on
1417
+ # the cached identity must still be re-pointed at the requested org.
1418
+ _rebind_home_tenant_identity(ctx, cache, org_info)
1359
1419
  return
1360
1420
 
1361
1421
  # Step 4: Reject a misregistered home-tenant org BEFORE any identity lookup —
@@ -1368,9 +1428,14 @@ def _switch_by_organization(
1368
1428
  if org_info.tenant_id:
1369
1429
  existing_by_tenant = cache.get_identity_by_tenant(org_info.tenant_id)
1370
1430
  if existing_by_tenant and existing_by_tenant.account_username:
1371
- # Update org info and activate
1431
+ # Update org info and activate. Resolve the real storage key: a
1432
+ # self-service identity is stored under username#tenant_id (legacy
1433
+ # files: the bare username), never looked up by bare username alone.
1434
+ tenant_storage_key = (
1435
+ cache.resolve_stored_key(existing_by_tenant) or existing_by_tenant.account_username
1436
+ )
1372
1437
  cache.update_identity_org_info(
1373
- existing_by_tenant.account_username,
1438
+ tenant_storage_key,
1374
1439
  org_info.id,
1375
1440
  org_info.name,
1376
1441
  )
@@ -1410,10 +1475,11 @@ def _activate_existing_identity(
1410
1475
  ) -> None:
1411
1476
  """Activate an existing cached identity."""
1412
1477
  # The identity's computed identity_key can diverge from its actual storage
1413
- # key: a customer external identity is stored under the bare username (org
1414
- # name unknown at save time), then discovery sets the org name, which makes
1415
- # identity_key recompute to username#org while the stored key stays bare.
1416
- # Resolve the real storage key for all cache operations.
1478
+ # key: a customer external identity is stored under username#tenant_id
1479
+ # (legacy files: the bare username) because the org name is unknown at save
1480
+ # time; discovery then sets the org name, which makes identity_key recompute
1481
+ # to username#org while the stored key never changes. Resolve the real
1482
+ # storage key for all cache operations.
1417
1483
  storage_key = cache.resolve_stored_key(identity) or identity.identity_key
1418
1484
 
1419
1485
  # Check if token is expired
@@ -1481,6 +1547,96 @@ def _fetch_organization_by_name(ctx: DxsContext, org_name: str) -> Any:
1481
1547
  return None
1482
1548
 
1483
1549
 
1550
+ def _rebind_home_tenant_identity(
1551
+ ctx: DxsContext,
1552
+ cache: MultiIdentityTokenCache,
1553
+ org_info: Any,
1554
+ ) -> None:
1555
+ """Re-point the active home-tenant identity at ``org_info`` (issue #112).
1556
+
1557
+ A home-tenant org needs no device login, but "no login required" never
1558
+ meant "no action required": the organization binding lives on the cached
1559
+ identity, and the API resolves the caller's user record through it. Reads
1560
+ against Datex-owned resources work under the old binding — which is what
1561
+ made the silent no-op hard to spot — while writes fail with a misleading
1562
+ ``DXS-API-400``. Only the two org fields move; tokens and the active
1563
+ identity selection stay untouched (the issue #83 contract).
1564
+ """
1565
+ active = cache.get_active_identity()
1566
+ if active is None or identity_needs_tenant_authority(active):
1567
+ # Nothing safe to re-bind: either no identity is cached, or the active
1568
+ # identity belongs to a foreign tenant and its tokens cannot serve a
1569
+ # home-tenant org. Fail closed rather than claim success.
1570
+ described = (
1571
+ f"the active identity '{active.account_username}' belongs to another tenant"
1572
+ if active
1573
+ else "no identity is cached"
1574
+ )
1575
+ ctx.output_error(
1576
+ AuthenticationError(
1577
+ message=(
1578
+ f"Organization '{org_info.name}' authenticates against the Datex "
1579
+ f"home tenant, but {described}"
1580
+ ),
1581
+ code="DXS-AUTH-018",
1582
+ suggestions=[
1583
+ "Run 'dxs auth switch <username>' to activate a home-tenant identity first",
1584
+ f"Or run 'dxs auth login {org_info.name}' to sign in with one",
1585
+ ],
1586
+ )
1587
+ )
1588
+ raise SystemExit(1)
1589
+
1590
+ already_bound = active.organization_id == org_info.id and (
1591
+ (active.organization_name or "").lower() == org_info.name.lower()
1592
+ )
1593
+ if already_bound:
1594
+ ctx.output(
1595
+ single(
1596
+ item={
1597
+ "status": "no_action_required",
1598
+ "message": (
1599
+ f"Active identity is already bound to organization '{org_info.name}'"
1600
+ ),
1601
+ "active_identity": active.account_username,
1602
+ "organization": org_info.name,
1603
+ "organization_id": org_info.id,
1604
+ },
1605
+ semantic_key="authentication",
1606
+ )
1607
+ )
1608
+ return
1609
+
1610
+ storage_key = cache.resolve_stored_key(active) or active.identity_key
1611
+ updated = cache.update_identity_org_info(storage_key, org_info.id, org_info.name)
1612
+ if updated is None:
1613
+ ctx.output_error(
1614
+ AuthenticationError(
1615
+ message=f"Identity '{storage_key}' not found",
1616
+ code="DXS-AUTH-010",
1617
+ )
1618
+ )
1619
+ raise SystemExit(1)
1620
+
1621
+ ctx.output(
1622
+ single(
1623
+ item={
1624
+ "status": "organization_rebound",
1625
+ "message": (
1626
+ f"Re-bound the active identity to organization '{org_info.name}' "
1627
+ "(home tenant; no login required)"
1628
+ ),
1629
+ "active_identity": active.account_username,
1630
+ "organization": org_info.name,
1631
+ "organization_id": org_info.id,
1632
+ "previous_organization": active.organization_name,
1633
+ "previous_organization_id": active.organization_id,
1634
+ },
1635
+ semantic_key="authentication",
1636
+ )
1637
+ )
1638
+
1639
+
1484
1640
  def _handled_home_tenant_external_org(ctx: DxsContext, org_info: Any) -> bool:
1485
1641
  """Report and stop when an org flagged external points at the home tenant.
1486
1642
 
@@ -1642,9 +1798,10 @@ def _login_to_organization(
1642
1798
  # First, we need to be able to call the API - check if we have any valid identity
1643
1799
  active = cache.get_active_identity()
1644
1800
  if not active or active.is_expired():
1645
- # Try to refresh
1801
+ # Try to refresh (no username -> the active identity, whatever its
1802
+ # storage key; the bare username is not the key for external identities)
1646
1803
  if active:
1647
- cache.try_refresh_identity(active.account_username)
1804
+ cache.try_refresh_identity()
1648
1805
  active = cache.get_active_identity()
1649
1806
 
1650
1807
  if not active or active.is_expired():
@@ -1,5 +1,6 @@
1
1
  """Branch commands: dxs branch [list|show|create|roles|shell|validate|candelete|search]."""
2
2
 
3
+ from collections.abc import Callable
3
4
  from concurrent.futures import ThreadPoolExecutor, as_completed
4
5
  from datetime import datetime
5
6
  from typing import Any
@@ -7,7 +8,14 @@ from typing import Any
7
8
  import click
8
9
 
9
10
  from dxs.cli import DxsContext, pass_context
10
- from dxs.commands.validation_report import server_error_messages
11
+ from dxs.commands.branch_commit_guard import (
12
+ all_changed_refs,
13
+ fetch_commit_preflight,
14
+ guarded_commit,
15
+ modification_bucket,
16
+ validate_branch_or_raise,
17
+ verify_config_refs,
18
+ )
11
19
  from dxs.core.api import (
12
20
  ApiClient,
13
21
  ApplicationGroupEndpoints,
@@ -817,10 +825,13 @@ def candelete(ctx: DxsContext, branch_id: int) -> None:
817
825
  @click.argument("branch_id", type=int)
818
826
  @click.option(
819
827
  "--force",
828
+ "--yes",
820
829
  "-f",
830
+ "-y",
831
+ "force",
821
832
  is_flag=True,
822
833
  default=False,
823
- help="Skip confirmation and delete immediately",
834
+ help="Skip confirmation and delete immediately (--yes/-y are aliases)",
824
835
  )
825
836
  @pass_context
826
837
  @require_auth
@@ -837,7 +848,7 @@ def delete(ctx: DxsContext, branch_id: int, force: bool) -> None:
837
848
 
838
849
  \b
839
850
  Options:
840
- --force, -f Skip confirmation prompt
851
+ --force, -f Skip confirmation prompt (--yes/-y are aliases)
841
852
 
842
853
  \b
843
854
  Examples:
@@ -1362,22 +1373,7 @@ def publish(
1362
1373
  if not force:
1363
1374
  # Validate the branch first
1364
1375
  ctx.log(f"Validating branch {branch_id}...")
1365
- errors = client.post(BranchEndpoints.validate(branch_id))
1366
- if not isinstance(errors, list):
1367
- errors = [errors] if errors else []
1368
-
1369
- if errors:
1370
- error_messages = server_error_messages(errors)
1371
- suggestions = error_messages[:5]
1372
- if len(error_messages) > 5:
1373
- suggestions.append(
1374
- f"... and {len(error_messages) - 5} more; run 'dxs branch validate "
1375
- f"{branch_id}' for the full list"
1376
- )
1377
- raise ValidationError(
1378
- message=f"Branch {branch_id} has validation errors",
1379
- suggestions=suggestions,
1380
- )
1376
+ validate_branch_or_raise(client, branch_id)
1381
1377
 
1382
1378
  # Confirm publish
1383
1379
  if not click.confirm(
@@ -1426,6 +1422,55 @@ def publish(
1426
1422
  )
1427
1423
 
1428
1424
 
1425
+ def _commit_confirmation_summary(
1426
+ branch_name: str,
1427
+ branch_id: int,
1428
+ selected: list[str] | None,
1429
+ preflight: dict[str, Any],
1430
+ ) -> str:
1431
+ """Human-readable summary of what a commit is about to merge, for click.confirm."""
1432
+
1433
+ def _breakdown(configs: list[dict[str, Any]]) -> str:
1434
+ tallies = {"created": 0, "updated": 0, "deleted": 0}
1435
+ for cfg in configs:
1436
+ bucket = modification_bucket(str(cfg.get("modificationTypeId", "")))
1437
+ if bucket:
1438
+ tallies[bucket] += 1
1439
+ parts = [f"{count} {bucket}" for bucket, count in tallies.items() if count]
1440
+ return ", ".join(parts) if parts else "no modification details"
1441
+
1442
+ total = preflight["counts"]["total"]
1443
+ lines: list[str] = []
1444
+ if selected is None:
1445
+ lines.append(
1446
+ f"Commit branch '{branch_name}' (ID: {branch_id}) to Main: "
1447
+ f"all {total} changed config(s) ({_breakdown(preflight['configs'])})."
1448
+ )
1449
+ else:
1450
+ selected_set = set(selected)
1451
+ selected_configs = [
1452
+ c for c in preflight["configs"] if c.get("referenceName") in selected_set
1453
+ ]
1454
+ lines.append(
1455
+ f"Commit branch '{branch_name}' (ID: {branch_id}) to Main: "
1456
+ f"{len(selected)} of {total} changed config(s) ({_breakdown(selected_configs)})."
1457
+ )
1458
+ leftovers = [r for r in preflight["changed_refs"] if r not in selected_set]
1459
+ if leftovers:
1460
+ lines.append(
1461
+ f"{len(leftovers)} changed config(s) not selected will transfer to a new "
1462
+ f"feature branch: {', '.join(leftovers)}."
1463
+ )
1464
+ if preflight["non_config_changes"]:
1465
+ lines.append(
1466
+ "Non-config changes also on this branch: "
1467
+ + ", ".join(preflight["non_config_changes"])
1468
+ + "."
1469
+ )
1470
+ lines.append("Commit to Main?")
1471
+ return "\n".join(lines)
1472
+
1473
+
1429
1474
  @branch.command("commit")
1430
1475
  @click.argument("branch_id", type=int)
1431
1476
  @click.option("--title", "-t", required=True, help="Commit title")
@@ -1435,13 +1480,27 @@ def publish(
1435
1480
  "config_refs",
1436
1481
  multiple=True,
1437
1482
  help="Reference name of a changed config to commit (repeatable). "
1438
- "Omit with --all-changes to commit everything modified.",
1483
+ "Use --all-changes instead to commit every modified config.",
1439
1484
  )
1440
1485
  @click.option(
1441
1486
  "--all-changes",
1442
1487
  is_flag=True,
1443
1488
  default=False,
1444
- help="Commit all modified configs on the branch.",
1489
+ help="Commit all modified configs on the branch (their reference names are "
1490
+ "resolved from the branch's change list and sent explicitly).",
1491
+ )
1492
+ @click.option(
1493
+ "--force",
1494
+ "-f",
1495
+ is_flag=True,
1496
+ default=False,
1497
+ help="Skip validation and confirmation",
1498
+ )
1499
+ @click.option(
1500
+ "--dry-run",
1501
+ is_flag=True,
1502
+ default=False,
1503
+ help="Show what would be committed without mutating anything",
1445
1504
  )
1446
1505
  @pass_context
1447
1506
  @require_auth
@@ -1453,12 +1512,27 @@ def commit(
1453
1512
  description: str,
1454
1513
  config_refs: tuple[str, ...],
1455
1514
  all_changes: bool,
1515
+ force: bool,
1516
+ dry_run: bool,
1456
1517
  ) -> None:
1457
1518
  """Commit (merge) a feature branch into its Main branch.
1458
1519
 
1520
+ Committing is irreversible, so unless --force is given the command verifies
1521
+ the branch's organization, checks each --config ref against the branch's
1522
+ actual changes, validates the branch server-side, and asks for confirmation
1523
+ with a summary of what will be merged. A partial commit transfers the
1524
+ uncommitted leftovers to a new feature branch (reported as new_branch_id).
1525
+
1526
+ --all-changes resolves the branch's changed configs and commits that
1527
+ explicit list (the API requires one). It always fetches the branch's
1528
+ change list — --force skips only validation and confirmation — and fails
1529
+ when the branch has nothing changed.
1530
+
1459
1531
  \b
1460
1532
  Examples:
1461
1533
  dxs source branch commit 3101 --title "bump Module1" -d "Re-pin Module1" --config appConfig
1534
+ dxs source branch commit 3101 -t "release" -d "everything" --all-changes
1535
+ dxs source branch commit 3101 -t "bump" -d "re-pin" --config appConfig --dry-run
1462
1536
  """
1463
1537
  if not config_refs and not all_changes:
1464
1538
  raise ValidationError(
@@ -1470,21 +1544,114 @@ def commit(
1470
1544
  )
1471
1545
 
1472
1546
  client = ApiClient()
1473
- request_data: dict[str, Any] = {"title": title, "description": description}
1474
- if not all_changes:
1475
- request_data["configs"] = list(config_refs)
1476
1547
 
1477
- ctx.log(f"Committing branch {branch_id}...")
1478
- result = client.post(SourceControlEndpoints.commit(branch_id), data=request_data)
1548
+ if dry_run:
1549
+ ctx.log(f"Fetching changes for branch {branch_id}...")
1550
+ preflight = fetch_commit_preflight(client, branch_id)
1551
+ if all_changes:
1552
+ refs = all_changed_refs(preflight, branch_id)
1553
+ else:
1554
+ refs = list(config_refs)
1555
+ verify_config_refs(refs, preflight["changed_refs"], branch_id)
1556
+ item: dict[str, Any] = {
1557
+ "dry_run": True,
1558
+ "branch_id": branch_id,
1559
+ "title": title,
1560
+ "all_changes": all_changes,
1561
+ "configs_to_commit": refs,
1562
+ "changes": preflight["counts"],
1563
+ "non_config_changes": preflight["non_config_changes"],
1564
+ }
1565
+ if not all_changes:
1566
+ leftovers = [r for r in preflight["changed_refs"] if r not in set(refs)]
1567
+ if leftovers:
1568
+ item["configs_not_selected"] = leftovers
1569
+ item["note"] = (
1570
+ "The configs not selected would transfer to a new feature branch "
1571
+ "(newBranchId in the commit result)."
1572
+ )
1573
+ ctx.output(single(item=item, semantic_key="commit_result"))
1574
+ return
1479
1575
 
1480
- ctx.output(
1481
- single(
1482
- item={
1483
- "branch_id": branch_id,
1484
- "title": title,
1485
- "new_branch_id": (result or {}).get("newBranchId"),
1486
- "result": result,
1487
- },
1488
- semantic_key="commit_result",
1576
+ # Check organization mismatch - committing to Main requires ownership
1577
+ ctx.log(f"Fetching branch {branch_id}...")
1578
+ branch_info = client.get(BranchEndpoints.get(branch_id))
1579
+ branch_name = branch_info.get("name", f"Branch {branch_id}")
1580
+ repo_id = branch_info.get("applicationDefinitionId")
1581
+ if repo_id:
1582
+ ctx.log("Verifying organization access...")
1583
+ repo_info = client.get(RepoEndpoints.get(repo_id))
1584
+ org_id = repo_info.get("organizationId")
1585
+ org_name = (
1586
+ repo_info.get("organization", {}).get("name") if repo_info.get("organization") else None
1489
1587
  )
1588
+ if org_id:
1589
+ verify_org_match(org_id, org_name)
1590
+
1591
+ selected: list[str]
1592
+ confirm_cb: Callable[[], bool] | None = None
1593
+ if all_changes or not force:
1594
+ # Preflight: the commit endpoint rejects a body without configs, so
1595
+ # --all-changes needs the branch's change list to build the explicit
1596
+ # config list even under --force. The non-force path needs it anyway,
1597
+ # to check --config refs against the branch's actual changes and build
1598
+ # the confirmation summary from what the server says is modified.
1599
+ ctx.log(f"Fetching changes for branch {branch_id}...")
1600
+ preflight = fetch_commit_preflight(client, branch_id)
1601
+ if all_changes:
1602
+ selected = all_changed_refs(preflight, branch_id)
1603
+ else:
1604
+ selected = list(config_refs)
1605
+ verify_config_refs(selected, preflight["changed_refs"], branch_id)
1606
+
1607
+ if not force:
1608
+ prompt = _commit_confirmation_summary(
1609
+ branch_name, branch_id, None if all_changes else selected, preflight
1610
+ )
1611
+
1612
+ def _confirm() -> bool:
1613
+ return click.confirm(prompt)
1614
+
1615
+ confirm_cb = _confirm
1616
+ ctx.log(f"Validating branch {branch_id}...")
1617
+ else:
1618
+ selected = list(config_refs)
1619
+
1620
+ committed, result = guarded_commit(
1621
+ client,
1622
+ branch_id,
1623
+ title=title,
1624
+ description=description,
1625
+ configs=selected,
1626
+ validate=not force,
1627
+ confirm=confirm_cb,
1490
1628
  )
1629
+
1630
+ if not committed:
1631
+ ctx.output(
1632
+ single(
1633
+ item={"committed": False, "reason": "User cancelled"},
1634
+ semantic_key="commit_result",
1635
+ branch_id=branch_id,
1636
+ )
1637
+ )
1638
+ return
1639
+
1640
+ result = result or {}
1641
+ new_branch_id = result.get("newBranchId")
1642
+ item = {
1643
+ "committed": True,
1644
+ "branch_id": branch_id,
1645
+ "title": title,
1646
+ "new_branch_id": new_branch_id,
1647
+ "result": result,
1648
+ }
1649
+ if new_branch_id and result.get("transferredUncommittedChanges"):
1650
+ note = (
1651
+ f"Uncommitted changes were transferred to new feature branch {new_branch_id}; "
1652
+ f"the configs not committed here continue there."
1653
+ )
1654
+ item["note"] = note
1655
+ ctx.log(note)
1656
+
1657
+ ctx.output(single(item=item, semantic_key="commit_result"))