datex-studio-cli 0.4.19__tar.gz → 0.4.21__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 (217) hide show
  1. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/PKG-INFO +1 -1
  2. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/pyproject.toml +1 -1
  3. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/__init__.py +1 -1
  4. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/cli.py +110 -10
  5. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/auth.py +228 -39
  6. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/explore.py +1 -1
  7. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/init.py +21 -16
  8. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/source.py +10 -3
  9. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/client.py +23 -0
  10. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/datex_tenant.py +209 -6
  11. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/msal_client.py +127 -26
  12. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/client.py +2 -0
  13. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/client.py +5 -0
  14. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/config.py +16 -14
  15. datex_studio_cli-0.4.21/src/dxs/utils/update_check.py +452 -0
  16. datex_studio_cli-0.4.21/src/dxs/utils/versioning.py +22 -0
  17. datex_studio_cli-0.4.19/src/dxs/utils/update_check.py +0 -208
  18. datex_studio_cli-0.4.19/src/dxs/utils/versioning.py +0 -22
  19. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/.gitignore +0 -0
  20. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/CONTEXT.md +0 -0
  21. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/README.md +0 -0
  22. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/.env.example +0 -0
  23. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/Dockerfile +0 -0
  24. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/README.md +0 -0
  25. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/apply-local-cache.js +0 -0
  26. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/collector-config.yaml +0 -0
  27. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/docker-compose.yml +0 -0
  28. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/local-test-settings.json +0 -0
  29. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/remote-settings.json +0 -0
  30. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/report-creator.skill +0 -0
  31. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/scripts/analyze_corpus.py +0 -0
  32. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/scripts/generate_rdlx_schema.py +0 -0
  33. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/__main__.py +0 -0
  34. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/__init__.py +0 -0
  35. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/api.py +0 -0
  36. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/branch.py +0 -0
  37. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/branch_commit_guard.py +0 -0
  38. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/cascade.py +0 -0
  39. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/config.py +0 -0
  40. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/configuration.py +0 -0
  41. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/crm.py +0 -0
  42. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/crm_delivery.py +0 -0
  43. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/crm_time.py +0 -0
  44. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/datasource.py +0 -0
  45. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/devops.py +0 -0
  46. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/document.py +0 -0
  47. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/endpoint.py +0 -0
  48. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/env.py +0 -0
  49. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/function.py +0 -0
  50. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/marketplace.py +0 -0
  51. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/__init__.py +0 -0
  52. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/_common.py +0 -0
  53. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/api.py +0 -0
  54. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/create.py +0 -0
  55. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/data.py +0 -0
  56. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/pack.py +0 -0
  57. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/preview.py +0 -0
  58. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/odata.py +0 -0
  59. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/organization.py +0 -0
  60. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/proxy.py +0 -0
  61. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/reference.py +0 -0
  62. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/release_notes.py +0 -0
  63. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/repo.py +0 -0
  64. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/__init__.py +0 -0
  65. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/add_cmds.py +0 -0
  66. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/api.py +0 -0
  67. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/batch.py +0 -0
  68. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/create.py +0 -0
  69. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/data.py +0 -0
  70. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/dataset.py +0 -0
  71. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/edit.py +0 -0
  72. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/folder.py +0 -0
  73. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/preview.py +0 -0
  74. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/schema_cmds.py +0 -0
  75. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/reset.py +0 -0
  76. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/schema.py +0 -0
  77. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/servicepack.py +0 -0
  78. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/studio.py +0 -0
  79. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/telemetry.py +0 -0
  80. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/user.py +0 -0
  81. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/validation_report.py +0 -0
  82. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/context.py +0 -0
  83. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/__init__.py +0 -0
  84. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/agent_browser.py +0 -0
  85. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/__init__.py +0 -0
  86. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/app_config.py +0 -0
  87. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/endpoints.py +0 -0
  88. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/metadata_models.py +0 -0
  89. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/models.py +0 -0
  90. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/reference_graph.py +0 -0
  91. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/__init__.py +0 -0
  92. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/decorators.py +0 -0
  93. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/tenants.py +0 -0
  94. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/token_cache.py +0 -0
  95. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/cache.py +0 -0
  96. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/__init__.py +0 -0
  97. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_generator.py +0 -0
  98. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_models.py +0 -0
  99. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_shape.py +0 -0
  100. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/generator.py +0 -0
  101. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/models.py +0 -0
  102. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/parsers.py +0 -0
  103. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/resolver.py +0 -0
  104. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/type_def_parser.py +0 -0
  105. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/validator.py +0 -0
  106. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/__init__.py +0 -0
  107. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/flow_models.py +0 -0
  108. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/models.py +0 -0
  109. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/parsers.py +0 -0
  110. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/__init__.py +0 -0
  111. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/helpers.py +0 -0
  112. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/models.py +0 -0
  113. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/__init__.py +0 -0
  114. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/html.py +0 -0
  115. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  116. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/pagination.py +0 -0
  117. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/resolvers.py +0 -0
  118. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/time_entry.py +0 -0
  119. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/__init__.py +0 -0
  120. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/client.py +0 -0
  121. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/edmx_parser.py +0 -0
  122. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/metadata.py +0 -0
  123. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/__init__.py +0 -0
  124. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/generator.py +0 -0
  125. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/models.py +0 -0
  126. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/validator.py +0 -0
  127. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/graph.py +0 -0
  128. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/onboarding.py +0 -0
  129. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/__init__.py +0 -0
  130. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/csv_fmt.py +0 -0
  131. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/formatter.py +0 -0
  132. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/json_fmt.py +0 -0
  133. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/redaction.py +0 -0
  134. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/yaml_fmt.py +0 -0
  135. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/release_notes.py +0 -0
  136. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/responses.py +0 -0
  137. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/__init__.py +0 -0
  138. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/config.py +0 -0
  139. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/identity.py +0 -0
  140. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/recorder.py +0 -0
  141. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/scrubber.py +0 -0
  142. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/sender.py +0 -0
  143. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/models/__init__.py +0 -0
  144. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/__init__.py +0 -0
  145. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/folder.py +0 -0
  146. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/manifest.py +0 -0
  147. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/mocks.py +0 -0
  148. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/preview.py +0 -0
  149. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/scaffold.py +0 -0
  150. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/serve_lock.py +0 -0
  151. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/py.typed +0 -0
  152. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/__init__.py +0 -0
  153. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/datasource_binding.py +0 -0
  154. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/engine.py +0 -0
  155. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/field_parser.py +0 -0
  156. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/folder.py +0 -0
  157. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/manifest.py +0 -0
  158. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/models.py +0 -0
  159. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/preview.py +0 -0
  160. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/preview_arjs.py +0 -0
  161. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/schema.py +0 -0
  162. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/svg_renderer.py +0 -0
  163. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/templates/__init__.py +0 -0
  164. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  165. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  166. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/validate_arjs.py +0 -0
  167. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/validator.py +0 -0
  168. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/wrapper.py +0 -0
  169. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/__init__.py +0 -0
  170. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/click_options.py +0 -0
  171. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/clipboard.py +0 -0
  172. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/errors.py +0 -0
  173. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/filtering.py +0 -0
  174. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/image.py +0 -0
  175. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/paths.py +0 -0
  176. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/resolvers.py +0 -0
  177. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/responses.py +0 -0
  178. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/restricted.py +0 -0
  179. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/sorting.py +0 -0
  180. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/__init__.py +0 -0
  181. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/app.py +0 -0
  182. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/design.py +0 -0
  183. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/proxy_app.py +0 -0
  184. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/404/index.html +0 -0
  185. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/404.html +0 -0
  186. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
  187. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
  188. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  189. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  190. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  191. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
  192. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  193. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  194. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  195. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  196. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  197. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
  198. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
  199. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
  200. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
  201. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  202. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  203. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  204. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  205. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  206. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  207. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  208. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  209. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/capture/index.html +0 -0
  210. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/capture/index.txt +0 -0
  211. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/index.html +0 -0
  212. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/index.txt +0 -0
  213. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/index.html +0 -0
  214. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/index.txt +0 -0
  215. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/render-cli.js +0 -0
  216. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/report-preview.html +0 -0
  217. {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/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.19
3
+ Version: 0.4.21
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.19"
7
+ version = "0.4.21"
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.19"
3
+ __version__ = "0.4.21"
4
4
  __app_name__ = "dxs"
@@ -70,6 +70,17 @@ def _stdout_is_pipe() -> bool:
70
70
  return False
71
71
 
72
72
 
73
+ def _stdin_is_tty() -> bool:
74
+ """Whether stdin is interactive — gates confirmation prompts.
75
+
76
+ A helper for the same reason as ``_stdout_is_pipe``: tests patch it.
77
+ """
78
+ try:
79
+ return sys.stdin.isatty()
80
+ except (AttributeError, ValueError):
81
+ return False
82
+
83
+
73
84
  def _ensure_utf8_streams() -> None:
74
85
  """Force stdout/stderr to UTF-8 so non-ASCII output never crashes the CLI.
75
86
 
@@ -571,7 +582,7 @@ def register_commands() -> None:
571
582
  @click.pass_context
572
583
  def _after_command(ctx: click.Context, result: Any, **kwargs: Any) -> Any:
573
584
  """Run post-command hooks (telemetry success event + PyPI update notice)."""
574
- from dxs.utils.update_check import maybe_show_update_notice
585
+ from dxs.utils.update_check import maybe_show_update_notice, maybe_show_update_result
575
586
 
576
587
  dxs_ctx: DxsContext | None = ctx.find_object(DxsContext)
577
588
 
@@ -585,6 +596,13 @@ def _after_command(ctx: click.Context, result: Any, **kwargs: Any) -> Any:
585
596
  except Exception: # noqa: BLE001
586
597
  pass
587
598
 
599
+ # The background-upgrade outcome is the result of an explicit user
600
+ # action, so it is not gated by --no-update-check.
601
+ try:
602
+ maybe_show_update_result()
603
+ except Exception: # noqa: BLE001 - never let the update check break the CLI
604
+ pass
605
+
588
606
  skip = bool(dxs_ctx and dxs_ctx.no_update_check)
589
607
  try:
590
608
  maybe_show_update_notice(skip=skip)
@@ -606,13 +624,23 @@ def _ctx_extras(dxs_ctx: "DxsContext | None") -> dict[str, Any]:
606
624
 
607
625
 
608
626
  @cli.command("update")
609
- def update_cmd() -> None:
610
- """Check PyPI for a newer release and print the upgrade command.
611
-
612
- This command never performs the upgrade itself — run the printed
613
- command in a fresh shell. This avoids the running-binary file-lock
614
- issues that any in-process upgrade hits on Windows.
627
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
628
+ @click.option(
629
+ "--check",
630
+ "check_only",
631
+ is_flag=True,
632
+ help="Only check for a newer release and print the upgrade command.",
633
+ )
634
+ def update_cmd(yes: bool, check_only: bool) -> None:
635
+ """Upgrade dxs to the latest release on PyPI.
636
+
637
+ On macOS/Linux this process is replaced by the installer's upgrade
638
+ command (uv/pipx/pip), so its output appears right here. On Windows
639
+ the running dxs.exe locks its own files, so the upgrade runs in a
640
+ detached background helper after dxs exits — the outcome is reported
641
+ on the next dxs run, with details in ~/.datex/update.log.
615
642
  """
643
+ import shutil
616
644
  import time
617
645
 
618
646
  from dxs.utils.update_check import (
@@ -620,8 +648,14 @@ def update_cmd() -> None:
620
648
  _fetch_latest_version,
621
649
  _read_cache,
622
650
  _write_cache,
651
+ count_windows_dxs_processes,
652
+ detect_upgrade_argv,
623
653
  detect_upgrade_command,
654
+ is_dev_install,
624
655
  is_newer,
656
+ is_windows,
657
+ perform_upgrade_posix,
658
+ perform_upgrade_windows,
625
659
  )
626
660
 
627
661
  click.echo(f"Current version: {__version__}", err=True)
@@ -636,11 +670,8 @@ def update_cmd() -> None:
636
670
  click.echo("You are on the latest version.", err=True)
637
671
  return
638
672
 
639
- upgrade_cmd = detect_upgrade_command()
640
673
  click.echo("", err=True)
641
674
  click.echo(f" A new release of dxs is available: {__version__} → {latest}", err=True)
642
- click.echo(f" To upgrade, run: {upgrade_cmd}", err=True)
643
- click.echo(f" https://pypi.org/project/{PACKAGE_NAME}/{latest}/", err=True)
644
675
 
645
676
  # Refresh the cache and suppress the auto-banner for the next dedupe
646
677
  # window — the user has explicitly seen the news.
@@ -651,6 +682,75 @@ def update_cmd() -> None:
651
682
  cache["last_notified_at"] = now
652
683
  _write_cache(cache)
653
684
 
685
+ if check_only:
686
+ click.echo(f" To upgrade, run: {detect_upgrade_command()}", err=True)
687
+ click.echo(f" https://pypi.org/project/{PACKAGE_NAME}/{latest}/", err=True)
688
+ return
689
+
690
+ if is_dev_install():
691
+ click.echo(
692
+ "This is a development install (source checkout / editable). "
693
+ "Upgrade it with: git pull && uv sync",
694
+ err=True,
695
+ )
696
+ sys.exit(1)
697
+
698
+ argv = detect_upgrade_argv()
699
+ resolved = shutil.which(argv[0])
700
+ if resolved is None:
701
+ click.echo(
702
+ f"Cannot find '{argv[0]}' on PATH — install it or upgrade manually with: "
703
+ f"{detect_upgrade_command()}",
704
+ err=True,
705
+ )
706
+ sys.exit(1)
707
+ argv = [resolved, *argv[1:]]
708
+
709
+ if not yes:
710
+ if not _stdin_is_tty():
711
+ click.echo(
712
+ "Not an interactive session — re-run with --yes to upgrade.",
713
+ err=True,
714
+ )
715
+ sys.exit(1)
716
+ click.confirm(f" Upgrade to {latest} now?", abort=True, err=True)
717
+
718
+ if is_windows():
719
+ running = count_windows_dxs_processes()
720
+ if running is not None and running > 1:
721
+ click.echo(
722
+ f" Warning: {running} dxs processes are running (dxs studio, another "
723
+ "terminal?). The upgrade may fail until they all exit.",
724
+ err=True,
725
+ )
726
+ try:
727
+ log_path = perform_upgrade_windows(argv, latest)
728
+ except OSError as e:
729
+ click.echo(f"Failed to start the background upgrade: {e}", err=True)
730
+ sys.exit(1)
731
+ click.echo(" The upgrade will run in the background once dxs exits.", err=True)
732
+ click.echo(f" Log: {log_path}", err=True)
733
+ click.echo(
734
+ " The outcome will be reported the next time you run dxs — give it a minute.",
735
+ err=True,
736
+ )
737
+ return
738
+
739
+ click.echo(f" Running: {' '.join(argv)}", err=True)
740
+ # execvp never returns on success, so the result_callback (telemetry
741
+ # success event) won't run — record it here first.
742
+ try:
743
+ from dxs.core.telemetry import record_end
744
+
745
+ record_end(exit_code=0, ctx_extras={})
746
+ except Exception: # noqa: BLE001
747
+ pass
748
+ try:
749
+ perform_upgrade_posix(argv)
750
+ except OSError as e:
751
+ click.echo(f"Upgrade failed to start: {e}", err=True)
752
+ sys.exit(1)
753
+
654
754
 
655
755
  # Register commands when module loads
656
756
  register_commands()
@@ -15,6 +15,7 @@ from dxs.core.auth.datex_tenant import (
15
15
  is_datex_identity,
16
16
  )
17
17
  from dxs.core.auth.decorators import get_access_token
18
+ from dxs.core.auth.msal_client import ConsentOutcome
18
19
  from dxs.core.auth.tenants import (
19
20
  identity_needs_tenant_authority,
20
21
  is_foreign_tenant,
@@ -51,6 +52,117 @@ def _reject_tenant_override(ctx: DxsContext, command: str) -> None:
51
52
  raise SystemExit(1)
52
53
 
53
54
 
55
+ def consent_failure_items(outcomes: list[ConsentOutcome] | None) -> list[dict[str, Any]]:
56
+ """Map failed consent rounds to payload entries (empty when all succeeded).
57
+
58
+ AADSTS650057 means the resource is not listed on the CLI app registration —
59
+ no amount of retrying or user consent can fix that, so it carries an
60
+ explicit escalation suggestion (issue #135).
61
+ """
62
+ items: list[dict[str, Any]] = []
63
+ for outcome in outcomes or []:
64
+ if outcome.ok:
65
+ continue
66
+ entry: dict[str, Any] = {
67
+ "resource": outcome.resource,
68
+ "error": outcome.error,
69
+ "error_description": outcome.error_description,
70
+ }
71
+ if outcome.aadsts_code == "650057":
72
+ entry["suggestion"] = (
73
+ "This resource is not listed on the CLI app registration, so consent "
74
+ "cannot be requested for it — ask an Entra admin to grant tenant-wide "
75
+ "consent for the resource to the Datex Studio CLI app."
76
+ )
77
+ items.append(entry)
78
+ return items
79
+
80
+
81
+ def _run_targeted_consent(ctx: DxsContext, resource_key: str) -> None:
82
+ """(Re)consent one Datex-internal resource without a full re-login.
83
+
84
+ Backs ``dxs auth login --consent devops|crm`` — the recovery path for a
85
+ user whose login-time consent round was skipped or silently failed
86
+ (issue #136). Uses the cached Datex identity; the consent round requests
87
+ the *named* scope so dynamic user consent applies (issue #135).
88
+ """
89
+ from dxs.core.auth.datex_tenant import consent_datex_resource
90
+ from dxs.utils.config import get_settings
91
+
92
+ settings = get_settings()
93
+ if resource_key == "devops":
94
+ resource = "Azure DevOps"
95
+ consent_scopes = settings.azure_devops_scopes
96
+ acquire_scopes = settings.azure_devops_scopes
97
+ error_code = "DXS-DEVOPS-AUTH-001"
98
+ else:
99
+ if not settings.dynamics_crm_url:
100
+ ctx.output_error(
101
+ ValidationError(
102
+ message=(
103
+ "dynamics_crm_url is not configured; there is no Dynamics CRM "
104
+ "resource to consent to."
105
+ ),
106
+ code="DXS-DYNAMICS-CONFIG-001",
107
+ suggestions=[
108
+ "Run: dxs config set dynamics_crm_url https://yourorg.crm.dynamics.com",
109
+ "Then re-run: dxs auth login --consent crm",
110
+ ],
111
+ )
112
+ )
113
+ raise SystemExit(1)
114
+ resource = "Dynamics CRM"
115
+ consent_scopes = settings.dynamics_crm_consent_scopes
116
+ acquire_scopes = settings.dynamics_crm_scopes
117
+ error_code = "DXS-DYNAMICS-AUTH-001"
118
+
119
+ def on_device_code(device_code: dict[str, Any]) -> None:
120
+ ctx.output(
121
+ single(
122
+ item={
123
+ "status": "awaiting_authentication",
124
+ "action_required": {
125
+ "type": "device_code",
126
+ "instructions": (
127
+ f"Sign in with your Datex account to consent to {resource}"
128
+ ),
129
+ "url": device_code["verification_uri"],
130
+ "code": device_code["user_code"],
131
+ "expires_in_seconds": device_code["expires_in"],
132
+ },
133
+ "message": device_code.get("message", ""),
134
+ },
135
+ semantic_key="authentication",
136
+ ),
137
+ include_metadata=False,
138
+ )
139
+
140
+ try:
141
+ outcome = consent_datex_resource(
142
+ consent_scopes=consent_scopes,
143
+ acquire_scopes=acquire_scopes,
144
+ resource=resource,
145
+ error_code=error_code,
146
+ on_device_code=on_device_code,
147
+ )
148
+ except DxsError as e:
149
+ ctx.output_error(e)
150
+ raise SystemExit(1) from None
151
+
152
+ ctx.output(
153
+ single(
154
+ item={
155
+ "status": "consented",
156
+ "message": f"Consent for {resource} granted and verified.",
157
+ "resource": resource,
158
+ "account": outcome["identity"],
159
+ "has_token": True,
160
+ },
161
+ semantic_key="authentication",
162
+ )
163
+ )
164
+
165
+
54
166
  def run_standard_device_login(
55
167
  ctx: DxsContext,
56
168
  on_device_code: Callable[[dict[str, Any]], None],
@@ -64,6 +176,7 @@ def run_standard_device_login(
64
176
  organization_id: int | None = None,
65
177
  organization_name: str | None = None,
66
178
  api_base_url: str | None = None,
179
+ consent_outcomes: list[ConsentOutcome] | None = None,
67
180
  ) -> CachedIdentity:
68
181
  """Run the multi-resource device-code login and save the identity.
69
182
 
@@ -88,6 +201,11 @@ def run_standard_device_login(
88
201
  that target a non-default environment (e.g. ``dxs init --target qa``) must
89
202
  pass it so the identity is stamped from the same environment its profile is
90
203
  detected against, not the stale lru_cached default.
204
+
205
+ ``consent_outcomes``, when given, receives one entry per additional consent
206
+ round so callers can include failures in their success payload. Failed
207
+ rounds are always logged as warnings here regardless — an additional
208
+ resource failing never aborts the login, so this is the only signal.
91
209
  """
92
210
  from dxs.utils.config import get_settings
93
211
 
@@ -117,25 +235,36 @@ def run_standard_device_login(
117
235
  tenant is known, so DevOps/CRM consent is requested only for Datex
118
236
  staff — a customer must never be prompted for Datex-internal
119
237
  resources by name.
238
+
239
+ The CRM round requests the *named* scope, not ``.default``:
240
+ ``.default`` is static consent and fails with AADSTS650057 for any
241
+ first-time user because Dataverse is not listed on the CLI app
242
+ registration (issue #135). Token acquisition keeps ``.default``.
120
243
  """
121
244
  internal = (
122
245
  [
123
246
  settings.azure_devops_scopes,
124
- settings.dynamics_crm_scopes,
125
- settings.dynamics_crm_admin_scopes,
126
- settings.data_export_service_scopes,
247
+ settings.dynamics_crm_consent_scopes,
127
248
  ]
128
249
  if claims_are_datex(token_response.get("id_token_claims") or {})
129
250
  else []
130
251
  )
131
252
  return [scopes for scopes in [*internal, [settings.footprint_scope]] if scopes]
132
253
 
254
+ outcomes: list[ConsentOutcome] = consent_outcomes if consent_outcomes is not None else []
133
255
  result = client.authenticate_device_code_multi_resource(
134
256
  primary_scopes=settings.azure_scopes,
135
257
  additional_scope_sets=additional_scope_sets,
136
258
  on_device_code=on_device_code,
137
259
  on_additional_consent_needed=consent_cb,
260
+ consent_outcomes=outcomes,
138
261
  )
262
+ for entry in consent_failure_items(outcomes):
263
+ detail = entry["error_description"] or entry["error"] or "unknown error"
264
+ warning = f"WARNING: consent for {entry['resource']} was not obtained: {detail}"
265
+ if "suggestion" in entry:
266
+ warning += f" {entry['suggestion']}"
267
+ ctx.log(warning)
139
268
 
140
269
  saved_identity = cache.save_identity(
141
270
  result,
@@ -173,13 +302,16 @@ def _auth_success_item(
173
302
  saved_identity: CachedIdentity,
174
303
  *,
175
304
  message: str = "Successfully authenticated",
305
+ consent_outcomes: list[ConsentOutcome] | None = None,
176
306
  ) -> dict[str, Any]:
177
307
  """Build the standard ``authenticated`` response payload shared by the login paths.
178
308
 
179
309
  The external-tenant descriptor is derived from the saved identity — never from
180
310
  which CLI flags were passed — so the payload cannot disagree with the cache
181
311
  (a --tenant-id login with the home tenant is persisted as internal). Adds the
182
- ``footprint_daemon_auth`` marker when the identity carries daemon creds.
312
+ ``footprint_daemon_auth`` marker when the identity carries daemon creds, and
313
+ ``consent_failures`` when an additional consent round failed — the primary
314
+ login succeeded, so this is where a missing consent becomes visible.
183
315
  """
184
316
  item: dict[str, Any] = {
185
317
  "status": "authenticated",
@@ -194,6 +326,9 @@ def _auth_success_item(
194
326
  item["expires_at"] = saved_identity.expires_at.isoformat()
195
327
  if saved_identity.footprint_client_id:
196
328
  item["footprint_daemon_auth"] = "configured"
329
+ failures = consent_failure_items(consent_outcomes)
330
+ if failures:
331
+ item["consent_failures"] = failures
197
332
  return item
198
333
 
199
334
 
@@ -279,6 +414,16 @@ def auth() -> None:
279
414
  default=None,
280
415
  help="Entra domain (e.g. csw-wi.com) — resolves the tenant ID for you",
281
416
  )
417
+ @click.option(
418
+ "--consent",
419
+ "consent_resource",
420
+ type=click.Choice(["devops", "crm"]),
421
+ default=None,
422
+ help=(
423
+ "Only (re)run the consent round for one Datex-internal resource, using the "
424
+ "cached Datex identity — no full re-login"
425
+ ),
426
+ )
282
427
  @pass_context
283
428
  def login(
284
429
  ctx: DxsContext,
@@ -287,6 +432,7 @@ def login(
287
432
  client_secret: str | None,
288
433
  tenant_id: str | None,
289
434
  domain: str | None,
435
+ consent_resource: str | None,
290
436
  ) -> None:
291
437
  """Authenticate with Azure Entra using device code flow.
292
438
 
@@ -307,6 +453,9 @@ def login(
307
453
  credentials) authentication for the Footprint API, needed for cross-tenant
308
454
  access where OBO flow is not available.
309
455
 
456
+ Use --consent devops|crm (Datex staff) to grant a missing consent for one
457
+ internal resource without re-running the whole login.
458
+
310
459
  \b
311
460
  Examples:
312
461
  dxs auth login # Login to default tenant
@@ -314,6 +463,7 @@ def login(
314
463
  dxs auth login --domain csw-wi.com # Bootstrap: no identity required
315
464
  dxs auth login --tenant-id <guid> # Same, when you know the GUID
316
465
  dxs auth login CAG --client-id <id> --client-secret <secret>
466
+ dxs auth login --consent devops # Grant the missing DevOps consent only
317
467
  """
318
468
  _reject_tenant_override(ctx, "login")
319
469
 
@@ -331,6 +481,23 @@ def login(
331
481
  )
332
482
  raise SystemExit(1)
333
483
 
484
+ if consent_resource:
485
+ if org or tenant_id or domain or client_id or client_secret:
486
+ ctx.output_error(
487
+ ValidationError(
488
+ message=(
489
+ "--consent runs a single consent round for the cached Datex "
490
+ "identity; it cannot be combined with an organization, tenant, "
491
+ "or daemon credentials"
492
+ ),
493
+ code="DXS-AUTH-024",
494
+ suggestions=["Run 'dxs auth login --consent devops|crm' by itself"],
495
+ )
496
+ )
497
+ raise SystemExit(1)
498
+ _run_targeted_consent(ctx, consent_resource)
499
+ return
500
+
334
501
  if org and (tenant_id or domain):
335
502
  ctx.output_error(
336
503
  ValidationError(
@@ -380,6 +547,7 @@ def login(
380
547
  include_metadata=False,
381
548
  )
382
549
 
550
+ external_outcomes: list[ConsentOutcome] = []
383
551
  try:
384
552
  saved_identity = run_standard_device_login(
385
553
  ctx,
@@ -388,12 +556,15 @@ def login(
388
556
  client_secret=client_secret,
389
557
  tenant_id=tenant_id,
390
558
  domain=domain,
559
+ consent_outcomes=external_outcomes,
391
560
  )
392
561
  except AuthenticationError as e:
393
562
  ctx.output_error(e)
394
563
  raise SystemExit(1) from None
395
564
 
396
- external_success_item = _auth_success_item(saved_identity)
565
+ external_success_item = _auth_success_item(
566
+ saved_identity, consent_outcomes=external_outcomes
567
+ )
397
568
  ctx.output(single(item=external_success_item, semantic_key="authentication"))
398
569
  return
399
570
 
@@ -438,6 +609,7 @@ def login(
438
609
  include_metadata=False,
439
610
  )
440
611
 
612
+ login_outcomes: list[ConsentOutcome] = []
441
613
  try:
442
614
  saved_identity = run_standard_device_login(
443
615
  ctx,
@@ -445,12 +617,13 @@ def login(
445
617
  client_id=client_id,
446
618
  client_secret=client_secret,
447
619
  on_additional_consent_needed=on_additional_consent_needed,
620
+ consent_outcomes=login_outcomes,
448
621
  )
449
622
  except AuthenticationError as e:
450
623
  ctx.output_error(e)
451
624
  raise SystemExit(1) from None
452
625
 
453
- success_item = _auth_success_item(saved_identity)
626
+ success_item = _auth_success_item(saved_identity, consent_outcomes=login_outcomes)
454
627
  ctx.output(single(item=success_item, semantic_key="authentication"))
455
628
 
456
629
 
@@ -1415,7 +1588,9 @@ def _switch_by_organization(
1415
1588
  # Home-tenant org (Datex, or an org with no tenant of its own): the
1416
1589
  # current identity's tokens already cover it, but the org binding on
1417
1590
  # the cached identity must still be re-pointed at the requested org.
1418
- _rebind_home_tenant_identity(ctx, cache, org_info)
1591
+ _rebind_home_tenant_identity(
1592
+ ctx, cache, org_info, client_id=client_id, client_secret=client_secret
1593
+ )
1419
1594
  return
1420
1595
 
1421
1596
  # Step 4: Reject a misregistered home-tenant org BEFORE any identity lookup —
@@ -1551,6 +1726,8 @@ def _rebind_home_tenant_identity(
1551
1726
  ctx: DxsContext,
1552
1727
  cache: MultiIdentityTokenCache,
1553
1728
  org_info: Any,
1729
+ client_id: str | None = None,
1730
+ client_secret: str | None = None,
1554
1731
  ) -> None:
1555
1732
  """Re-point the active home-tenant identity at ``org_info`` (issue #112).
1556
1733
 
@@ -1561,6 +1738,10 @@ def _rebind_home_tenant_identity(
1561
1738
  made the silent no-op hard to spot — while writes fail with a misleading
1562
1739
  ``DXS-API-400``. Only the two org fields move; tokens and the active
1563
1740
  identity selection stay untouched (the issue #83 contract).
1741
+
1742
+ Supplied daemon credentials are persisted on the active identity on both
1743
+ outcomes (issue #133) — the same contract the other org-switch branches
1744
+ honor. Without credentials, the already-bound outcome writes nothing.
1564
1745
  """
1565
1746
  active = cache.get_active_identity()
1566
1747
  if active is None or identity_needs_tenant_authority(active):
@@ -1587,27 +1768,29 @@ def _rebind_home_tenant_identity(
1587
1768
  )
1588
1769
  raise SystemExit(1)
1589
1770
 
1771
+ storage_key = cache.resolve_stored_key(active) or active.identity_key
1772
+
1590
1773
  already_bound = active.organization_id == org_info.id and (
1591
1774
  (active.organization_name or "").lower() == org_info.name.lower()
1592
1775
  )
1593
1776
  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
- )
1777
+ identity = active
1778
+ if client_id and client_secret:
1779
+ with_creds = cache.update_identity_daemon_creds(storage_key, client_id, client_secret)
1780
+ if with_creds is not None:
1781
+ identity = with_creds
1782
+ item: dict[str, Any] = {
1783
+ "status": "no_action_required",
1784
+ "message": (f"Active identity is already bound to organization '{org_info.name}'"),
1785
+ "active_identity": active.account_username,
1786
+ "organization": org_info.name,
1787
+ "organization_id": org_info.id,
1788
+ }
1789
+ if identity.footprint_client_id:
1790
+ item["footprint_daemon_auth"] = "configured"
1791
+ ctx.output(single(item=item, semantic_key="authentication"))
1608
1792
  return
1609
1793
 
1610
- storage_key = cache.resolve_stored_key(active) or active.identity_key
1611
1794
  updated = cache.update_identity_org_info(storage_key, org_info.id, org_info.name)
1612
1795
  if updated is None:
1613
1796
  ctx.output_error(
@@ -1618,23 +1801,26 @@ def _rebind_home_tenant_identity(
1618
1801
  )
1619
1802
  raise SystemExit(1)
1620
1803
 
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
- )
1804
+ if client_id and client_secret:
1805
+ with_creds = cache.update_identity_daemon_creds(storage_key, client_id, client_secret)
1806
+ if with_creds is not None:
1807
+ updated = with_creds
1808
+
1809
+ item = {
1810
+ "status": "organization_rebound",
1811
+ "message": (
1812
+ f"Re-bound the active identity to organization '{org_info.name}' "
1813
+ "(home tenant; no login required)"
1814
+ ),
1815
+ "active_identity": active.account_username,
1816
+ "organization": org_info.name,
1817
+ "organization_id": org_info.id,
1818
+ "previous_organization": active.organization_name,
1819
+ "previous_organization_id": active.organization_id,
1820
+ }
1821
+ if updated.footprint_client_id:
1822
+ item["footprint_daemon_auth"] = "configured"
1823
+ ctx.output(single(item=item, semantic_key="authentication"))
1638
1824
 
1639
1825
 
1640
1826
  def _handled_home_tenant_external_org(ctx: DxsContext, org_info: Any) -> bool:
@@ -1726,6 +1912,7 @@ def _authenticate_external_tenant(
1726
1912
  include_metadata=False,
1727
1913
  )
1728
1914
 
1915
+ org_outcomes: list[ConsentOutcome] = []
1729
1916
  try:
1730
1917
  ctx.log(f"Initiating device code authentication for {org_info.name}...")
1731
1918
  saved_identity = run_standard_device_login(
@@ -1739,11 +1926,13 @@ def _authenticate_external_tenant(
1739
1926
  external_entra_id=org_info.external_entra_id,
1740
1927
  organization_id=org_info.id,
1741
1928
  organization_name=org_info.name,
1929
+ consent_outcomes=org_outcomes,
1742
1930
  )
1743
1931
 
1744
1932
  success_item = _auth_success_item(
1745
1933
  saved_identity,
1746
1934
  message=f"Successfully authenticated with {org_info.name}",
1935
+ consent_outcomes=org_outcomes,
1747
1936
  )
1748
1937
  ctx.output(single(item=success_item, semantic_key="authentication"))
1749
1938
 
@@ -337,7 +337,7 @@ def _build_config_index(client: ApiClient, branch_id: int, ctx: DxsContext) -> C
337
337
  )
338
338
  except ApiError as e:
339
339
  # Log JSON parse errors as warnings (e.g., endpoints type)
340
- if e.code == "DXS-API-JSON-001":
340
+ if e.code in ("DXS-API-JSON-001", "DXS-API-ROUTE-404"):
341
341
  ctx.log(f"Warning: {ctype} returned non-JSON response")
342
342
  # Other API errors are expected (type may not exist)
343
343
  except Exception: