datex-studio-cli 0.5.1__tar.gz → 0.5.3__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 (236) hide show
  1. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/ci.yml +29 -0
  2. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/PKG-INFO +1 -1
  3. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/pyproject.toml +3 -3
  4. datex_studio_cli-0.5.3/scripts/check_wheel_completeness.py +58 -0
  5. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/__init__.py +1 -1
  6. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/auth.py +28 -0
  7. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/onboarding.py +126 -2
  8. datex_studio_cli-0.5.3/src/dxs/core/quartermaster/__init__.py +5 -0
  9. datex_studio_cli-0.5.3/src/dxs/core/quartermaster/client.py +190 -0
  10. datex_studio_cli-0.5.3/src/dxs/core/quartermaster/tenants.py +57 -0
  11. datex_studio_cli-0.5.3/src/dxs/core/skills/__init__.py +1 -0
  12. datex_studio_cli-0.5.3/src/dxs/core/skills/frontmatter.py +30 -0
  13. datex_studio_cli-0.5.3/src/dxs/core/skills/installer.py +148 -0
  14. datex_studio_cli-0.5.3/src/dxs/core/skills/layout.py +57 -0
  15. datex_studio_cli-0.5.3/src/dxs/core/skills/lock.py +98 -0
  16. datex_studio_cli-0.5.3/src/dxs/core/skills/resolver.py +48 -0
  17. datex_studio_cli-0.5.3/src/dxs/core/skills/staleness.py +55 -0
  18. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/404/index.html +1 -1
  19. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/404.html +1 -1
  20. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/capture/index.html +1 -1
  21. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/capture/index.txt +1 -1
  22. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/index.html +1 -1
  23. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/index.txt +1 -1
  24. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/index.html +1 -1
  25. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/index.txt +1 -1
  26. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.dockerignore +0 -0
  27. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/actionlint.yaml +0 -0
  28. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/build-image.yml +0 -0
  29. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/promote-prod.yml +0 -0
  30. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/release.yml +0 -0
  31. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.gitignore +0 -0
  32. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/CONTEXT.md +0 -0
  33. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/README.md +0 -0
  34. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/.env.example +0 -0
  35. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/Dockerfile +0 -0
  36. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/README.md +0 -0
  37. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/apply-local-cache.js +0 -0
  38. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/collector-config.yaml +0 -0
  39. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/docker-compose.yml +0 -0
  40. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/local-test-settings.json +0 -0
  41. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/remote-settings.json +0 -0
  42. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/report-creator.skill +0 -0
  43. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/analyze_corpus.py +0 -0
  44. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/datadog_errors.py +0 -0
  45. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/generate_rdlx_schema.py +0 -0
  46. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/wavelength_query.py +0 -0
  47. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/__main__.py +0 -0
  48. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/cli.py +0 -0
  49. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/__init__.py +0 -0
  50. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/api.py +0 -0
  51. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/branch.py +0 -0
  52. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/branch_commit_guard.py +0 -0
  53. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/cascade.py +0 -0
  54. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/config.py +0 -0
  55. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/configuration.py +0 -0
  56. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/crm.py +0 -0
  57. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/crm_delivery.py +0 -0
  58. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/crm_time.py +0 -0
  59. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/datasource.py +0 -0
  60. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/devops.py +0 -0
  61. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/document.py +0 -0
  62. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/endpoint.py +0 -0
  63. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/env.py +0 -0
  64. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/explore.py +0 -0
  65. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/function.py +0 -0
  66. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/init.py +0 -0
  67. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/marketplace.py +0 -0
  68. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/__init__.py +0 -0
  69. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/_common.py +0 -0
  70. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/api.py +0 -0
  71. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/create.py +0 -0
  72. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/data.py +0 -0
  73. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/pack.py +0 -0
  74. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/preview.py +0 -0
  75. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/odata.py +0 -0
  76. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/organization.py +0 -0
  77. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/proxy.py +0 -0
  78. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/reference.py +0 -0
  79. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/release_notes.py +0 -0
  80. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/repo.py +0 -0
  81. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/__init__.py +0 -0
  82. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/add_cmds.py +0 -0
  83. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/api.py +0 -0
  84. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/batch.py +0 -0
  85. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/create.py +0 -0
  86. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/data.py +0 -0
  87. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/dataset.py +0 -0
  88. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/edit.py +0 -0
  89. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/folder.py +0 -0
  90. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/preview.py +0 -0
  91. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/schema_cmds.py +0 -0
  92. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/reset.py +0 -0
  93. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/schema.py +0 -0
  94. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/servicepack.py +0 -0
  95. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/skills.py +0 -0
  96. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/source.py +0 -0
  97. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/studio.py +0 -0
  98. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/telemetry.py +0 -0
  99. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/user.py +0 -0
  100. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/validation_report.py +0 -0
  101. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/context.py +0 -0
  102. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/__init__.py +0 -0
  103. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/agent_browser.py +0 -0
  104. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/__init__.py +0 -0
  105. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/app_config.py +0 -0
  106. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/client.py +0 -0
  107. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/endpoints.py +0 -0
  108. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/metadata_models.py +0 -0
  109. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/models.py +0 -0
  110. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/reference_graph.py +0 -0
  111. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/replacements.py +0 -0
  112. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/__init__.py +0 -0
  113. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/datex_tenant.py +0 -0
  114. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/decorators.py +0 -0
  115. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/msal_client.py +0 -0
  116. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/tenants.py +0 -0
  117. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/token_cache.py +0 -0
  118. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/cache.py +0 -0
  119. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/__init__.py +0 -0
  120. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/flow_generator.py +0 -0
  121. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/flow_models.py +0 -0
  122. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/flow_shape.py +0 -0
  123. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/generator.py +0 -0
  124. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/models.py +0 -0
  125. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/parsers.py +0 -0
  126. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/resolver.py +0 -0
  127. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/type_def_parser.py +0 -0
  128. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/validator.py +0 -0
  129. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/__init__.py +0 -0
  130. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/flow_models.py +0 -0
  131. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/models.py +0 -0
  132. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/parsers.py +0 -0
  133. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/__init__.py +0 -0
  134. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/client.py +0 -0
  135. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/helpers.py +0 -0
  136. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/models.py +0 -0
  137. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/__init__.py +0 -0
  138. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/client.py +0 -0
  139. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/html.py +0 -0
  140. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  141. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/pagination.py +0 -0
  142. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/resolvers.py +0 -0
  143. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/time_entry.py +0 -0
  144. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/__init__.py +0 -0
  145. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/client.py +0 -0
  146. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/edmx_parser.py +0 -0
  147. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/metadata.py +0 -0
  148. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/__init__.py +0 -0
  149. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/generator.py +0 -0
  150. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/models.py +0 -0
  151. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/validator.py +0 -0
  152. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/graph.py +0 -0
  153. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/__init__.py +0 -0
  154. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/csv_fmt.py +0 -0
  155. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/formatter.py +0 -0
  156. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/json_fmt.py +0 -0
  157. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/redaction.py +0 -0
  158. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/yaml_fmt.py +0 -0
  159. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/release_notes.py +0 -0
  160. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/responses.py +0 -0
  161. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/__init__.py +0 -0
  162. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/config.py +0 -0
  163. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/identity.py +0 -0
  164. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/recorder.py +0 -0
  165. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/scrubber.py +0 -0
  166. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/sender.py +0 -0
  167. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/models/__init__.py +0 -0
  168. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/__init__.py +0 -0
  169. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/folder.py +0 -0
  170. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/manifest.py +0 -0
  171. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/mocks.py +0 -0
  172. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/preview.py +0 -0
  173. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/scaffold.py +0 -0
  174. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/serve_lock.py +0 -0
  175. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/py.typed +0 -0
  176. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/__init__.py +0 -0
  177. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/datasource_binding.py +0 -0
  178. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/engine.py +0 -0
  179. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/field_parser.py +0 -0
  180. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/folder.py +0 -0
  181. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/manifest.py +0 -0
  182. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/models.py +0 -0
  183. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/preview.py +0 -0
  184. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/preview_arjs.py +0 -0
  185. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/schema.py +0 -0
  186. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/svg_renderer.py +0 -0
  187. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/templates/__init__.py +0 -0
  188. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  189. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  190. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/validate_arjs.py +0 -0
  191. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/validator.py +0 -0
  192. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/wrapper.py +0 -0
  193. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/__init__.py +0 -0
  194. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/click_options.py +0 -0
  195. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/clipboard.py +0 -0
  196. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/config.py +0 -0
  197. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/errors.py +0 -0
  198. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/filtering.py +0 -0
  199. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/image.py +0 -0
  200. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/paths.py +0 -0
  201. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/resolvers.py +0 -0
  202. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/responses.py +0 -0
  203. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/restricted.py +0 -0
  204. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/sorting.py +0 -0
  205. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/update_check.py +0 -0
  206. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/versioning.py +0 -0
  207. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/__init__.py +0 -0
  208. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/app.py +0 -0
  209. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/design.py +0 -0
  210. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/proxy_app.py +0 -0
  211. {datex_studio_cli-0.5.1/src/dxs/web/static/_next/static/W6-87g3VtSLhTM2G0U3r1 → datex_studio_cli-0.5.3/src/dxs/web/static/_next/static/Xsbcf46McNo4ycd40X4z0}/_buildManifest.js +0 -0
  212. {datex_studio_cli-0.5.1/src/dxs/web/static/_next/static/W6-87g3VtSLhTM2G0U3r1 → datex_studio_cli-0.5.3/src/dxs/web/static/_next/static/Xsbcf46McNo4ycd40X4z0}/_ssgManifest.js +0 -0
  213. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  214. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  215. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  216. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/538-b06224e65f71c9a1.js +0 -0
  217. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  218. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  219. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  220. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  221. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-e3c660196c244183.js +0 -0
  222. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-c083dc9f01f32a04.js +0 -0
  223. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/design/page-689cd5b9e1d9a74e.js +0 -0
  224. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/layout-cec86f90e385371d.js +0 -0
  225. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/page-f0380b0f29983264.js +0 -0
  226. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  227. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  228. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/main-app-694cf3568b6c9fa9.js +0 -0
  229. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  230. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  231. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  232. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  233. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  234. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/render-cli.js +0 -0
  235. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/report-preview.html +0 -0
  236. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/report-validate.html +0 -0
@@ -59,3 +59,32 @@ jobs:
59
59
  - run: uv run ruff check quartermaster
60
60
  - run: uv run --package quartermaster mypy quartermaster/src/quartermaster
61
61
  - run: uv run --package quartermaster pytest quartermaster/tests -q
62
+
63
+ # The artifact nobody was looking at. 0.5.0 and 0.5.1 both shipped without
64
+ # dxs.core.quartermaster and dxs.core.skills, and 0.5.1 could not run
65
+ # `dxs --version` at all -- while this suite was fully green, because tests
66
+ # import from the source tree and never open the wheel. release.yml calls
67
+ # this workflow, so a broken artifact now blocks the release too.
68
+ package:
69
+ runs-on: arc-dxs
70
+ timeout-minutes: 15
71
+ steps:
72
+ - uses: actions/checkout@v7
73
+ - uses: astral-sh/setup-uv@v10.0.1
74
+ with:
75
+ enable-cache: true
76
+ cache-suffix: package
77
+ download-from-astral-mirror: false
78
+ - run: uv python install 3.13
79
+ - run: uv build
80
+ - name: Every package under src/ must be in the wheel
81
+ run: uv run python scripts/check_wheel_completeness.py
82
+ - name: The built wheel must actually run
83
+ run: |
84
+ # A clean venv, not the workspace: importing from the source tree is
85
+ # exactly what hid this. Resolve deps from the index like a user does.
86
+ uv venv /tmp/smoke
87
+ uv pip install --python /tmp/smoke/bin/python dist/*.whl
88
+ /tmp/smoke/bin/dxs --version
89
+ /tmp/smoke/bin/dxs skills --help > /dev/null
90
+ /tmp/smoke/bin/python -c "import dxs.core.quartermaster, dxs.core.skills"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datex-studio-cli
3
- Version: 0.5.1
3
+ Version: 0.5.3
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.5.1"
7
+ version = "0.5.3"
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"
@@ -94,7 +94,7 @@ exclude = [
94
94
  "documentation/",
95
95
  "exploration/",
96
96
  "exclude/",
97
- "skills/",
97
+ "/skills/",
98
98
  "tests/",
99
99
  "publish.sh",
100
100
  "*.analysis.md",
@@ -102,7 +102,7 @@ exclude = [
102
102
  "uv.lock",
103
103
  "src/dxs/web/frontend/",
104
104
  "example-reports/",
105
- "quartermaster/",
105
+ "/quartermaster/",
106
106
  ]
107
107
 
108
108
  [tool.uv.workspace]
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python
2
+ """Assert the built wheel carries every package that exists under src/dxs.
3
+
4
+ Versions 0.5.0 and 0.5.1 both shipped without ``dxs.core.quartermaster`` and
5
+ ``dxs.core.skills``. The sdist exclude list named ``skills/`` and
6
+ ``quartermaster/`` to drop the top-level directories of those names; hatchling
7
+ matches an unanchored pattern at any depth, so it dropped the nested packages
8
+ too, and ``uv build`` builds the wheel from the sdist it just made. 0.5.1 could
9
+ not even run ``dxs --version``.
10
+
11
+ A full green suite said nothing about any of it: tests import from the source
12
+ tree and never open the artifact. This looks at the artifact.
13
+
14
+ Usage: check_wheel_completeness.py [dist_dir]
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ import zipfile
21
+ from pathlib import Path
22
+
23
+ ROOT = Path(__file__).resolve().parent.parent
24
+ SRC = ROOT / "src"
25
+
26
+
27
+ def main() -> int:
28
+ dist = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "dist"
29
+ wheels = sorted(dist.glob("*.whl"))
30
+ if not wheels:
31
+ print(f"no wheel in {dist} — run `uv build` first", file=sys.stderr)
32
+ return 1
33
+ wheel = wheels[-1]
34
+
35
+ # Every directory holding an __init__.py is a package that must ship.
36
+ expected = {
37
+ p.parent.relative_to(SRC).as_posix() for p in SRC.rglob("__init__.py")
38
+ if "__pycache__" not in p.parts
39
+ }
40
+ names = zipfile.ZipFile(wheel).namelist()
41
+ shipped = {n.rsplit("/", 1)[0] for n in names if n.endswith("/__init__.py")}
42
+
43
+ missing = sorted(expected - shipped)
44
+ if missing:
45
+ print(f"{wheel.name} is missing {len(missing)} package(s) present in src/:",
46
+ file=sys.stderr)
47
+ for m in missing:
48
+ print(f" {m}", file=sys.stderr)
49
+ print("\nUsually an unanchored exclude pattern in pyproject.toml matching a "
50
+ "nested path — anchor it with a leading slash.", file=sys.stderr)
51
+ return 1
52
+
53
+ print(f"{wheel.name}: all {len(expected)} packages present")
54
+ return 0
55
+
56
+
57
+ if __name__ == "__main__":
58
+ raise SystemExit(main())
@@ -1,4 +1,4 @@
1
1
  """Datex Studio CLI - Command-line interface for Datex Studio platform."""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.5.3"
4
4
  __app_name__ = "dxs"
@@ -32,14 +32,38 @@ from dxs.core.auth.token_cache import CachedIdentity, get_identity_override
32
32
  from dxs.core.footprint import get_footprint_token
33
33
  from dxs.core.onboarding import (
34
34
  LoginPresenter,
35
+ apply_sign_in_tenant_hint,
35
36
  human_present,
36
37
  map_login_failure,
38
+ missing_service_principal_tenant,
37
39
  )
40
+ from dxs.core.quartermaster.tenants import lookup_sign_in_tenant
38
41
  from dxs.utils.errors import AuthenticationError, DxsError, ValidationError
39
42
  from dxs.utils.responses import list_response, single
40
43
  from dxs.utils.restricted import restrict_in_restricted_mode
41
44
 
42
45
 
46
+ def _sharpen_missing_service_principal(
47
+ mapped: AuthenticationError, tenant_id: str | None, raw_error: str
48
+ ) -> AuthenticationError:
49
+ """Ask Quartermaster which tenant the org actually signs in against.
50
+
51
+ Only for DXS-AUTH-030, only after the login already failed, and only ever
52
+ additive: any problem reaching the service leaves the mapped notice as-is.
53
+ The tenant comes from the caller when it resolved one, and otherwise out of
54
+ Entra's own error text — a blank-prompt or org-name login never had one.
55
+ """
56
+ requested = tenant_id or missing_service_principal_tenant(raw_error)
57
+ if not requested:
58
+ return mapped
59
+ try:
60
+ return apply_sign_in_tenant_hint(
61
+ mapped, requested_tenant=requested, sign_in=lookup_sign_in_tenant(requested)
62
+ )
63
+ except Exception: # noqa: BLE001 - never worsen a diagnosed failure
64
+ return mapped
65
+
66
+
43
67
  def _reject_tenant_override(ctx: DxsContext, command: str) -> None:
44
68
  """Block --tenant on identity-management commands.
45
69
 
@@ -333,6 +357,10 @@ def run_standard_login(
333
357
  mapped = map_login_failure(
334
358
  e, external=tenant_scoped, tenant_id=tenant_id, domain=domain, upn=upn
335
359
  )
360
+ if mapped.code == "DXS-AUTH-030":
361
+ mapped = _sharpen_missing_service_principal(
362
+ mapped, tenant_id, f"{e.message} {e.details}"
363
+ )
336
364
  if mapped is e:
337
365
  raise
338
366
  raise mapped from e
@@ -8,6 +8,7 @@ keeps a hard rule: machine envelopes go to stdout, human chrome to stderr.
8
8
  """
9
9
 
10
10
  import os
11
+ import re
11
12
  import shutil
12
13
  import sys
13
14
  from dataclasses import dataclass, field
@@ -19,6 +20,7 @@ import click
19
20
  if TYPE_CHECKING:
20
21
  from dxs.context import DxsContext
21
22
  from dxs.core.api.client import ApiClient
23
+ from dxs.core.quartermaster.tenants import SignInTenant
22
24
  from dxs.utils.errors import AuthenticationError
23
25
 
24
26
 
@@ -151,6 +153,51 @@ def detect_profile(client: "ApiClient") -> OnboardingProfile:
151
153
  )
152
154
 
153
155
 
156
+ # AADSTS650052: the tenant holds no service principal for a Datex app, so the
157
+ # sign-in cannot even reach a consent screen. Its prose ("Contact your IT Admin
158
+ # ... or consent to the application") reads as consent-required and is not: no
159
+ # consent granted in the tenant named by the error helps a user whose account
160
+ # lives in a different one, which is the common cause — a work-email domain
161
+ # resolves to the org's own corporate tenant (`homeTenantId`), while Datex
162
+ # provisioned `wavelength<org>.onmicrosoft.com` for them and invited their staff
163
+ # into that. Detected and answered ahead of the consent family for that reason.
164
+ _MISSING_SP_MARKERS = (
165
+ "aadsts650052",
166
+ "lacks a service principal",
167
+ )
168
+
169
+ # The tenant is named in the error text, which is the only source on paths where
170
+ # the caller never resolved one (a blank prompt, an org-name login).
171
+ _MISSING_SP_TENANT = re.compile(
172
+ r"organization\s+'?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'?",
173
+ re.IGNORECASE,
174
+ )
175
+
176
+
177
+ def is_missing_service_principal_error(error_text: str) -> bool:
178
+ """True when the tenant signed in against has no Datex service principal."""
179
+ text = (error_text or "").lower()
180
+ return any(marker in text for marker in _MISSING_SP_MARKERS)
181
+
182
+
183
+ def missing_service_principal_tenant(error_text: str) -> str | None:
184
+ """The tenant ID Entra named in an AADSTS650052 failure, if it is there."""
185
+ match = _MISSING_SP_TENANT.search(error_text or "")
186
+ return match.group(1) if match else None
187
+
188
+
189
+ def render_missing_service_principal_notice(tenant_id: str, domain: str | None) -> str:
190
+ """Actionable notice for a sign-in scoped to a tenant Datex does not live in."""
191
+ where = f" ({domain})" if domain else ""
192
+ tenant = tenant_id or "the tenant you signed in against"
193
+ return (
194
+ f"AADSTS650052: tenant {tenant}{where} has no service principal for the Datex apps, "
195
+ "so no Datex sign-in against it can succeed. A work-email domain resolves to your "
196
+ "organization's own corporate tenant, which is not where your Datex account lives "
197
+ "when Datex provisioned a wavelength<org>.onmicrosoft.com tenant for your organization."
198
+ )
199
+
200
+
154
201
  # Admin-consent-required failure markers. AADSTS65001 = user/admin has not consented;
155
202
  # AADSTS90094 = admin consent required. VERIFY these against a real un-consented tenant
156
203
  # during rollout — device-code flow may surface the code in either the primary result
@@ -165,8 +212,15 @@ _CONSENT_ERROR_MARKERS = (
165
212
 
166
213
 
167
214
  def is_consent_required_error(error_text: str) -> bool:
168
- """True when an auth failure indicates the app is not consented in the tenant."""
215
+ """True when an auth failure indicates the app is not consented in the tenant.
216
+
217
+ A missing service principal (AADSTS650052) is deliberately excluded: its text
218
+ carries both "consent" and "IT Admin", so the heuristic below claims it, and
219
+ every remedy that follows from that answer points at the wrong tenant.
220
+ """
169
221
  text = (error_text or "").lower()
222
+ if is_missing_service_principal_error(text):
223
+ return False
170
224
  if any(marker in text for marker in _CONSENT_ERROR_MARKERS):
171
225
  return True
172
226
  # Catch phrasings like "admin approval required" / "has not consented".
@@ -248,7 +302,9 @@ def map_login_failure(
248
302
  are external-tenant only: both are meaningless for the home tenant, where
249
303
  the app is consented and every user is a member. The Conditional Access
250
304
  notice is not, because that policy is configured per tenant and the home
251
- tenant can adopt it like any other.
305
+ tenant can adopt it like any other. Neither is the missing-service-principal
306
+ notice: it names the tenant Entra refused, which it carries in its own text,
307
+ so it stays diagnostic on the paths where the caller resolved no tenant at all.
252
308
  """
253
309
  from dxs.utils.errors import AuthenticationError
254
310
 
@@ -266,6 +322,22 @@ def map_login_failure(
266
322
  "If you are on a remote host, sign in locally and copy ~/.datex/credentials.yaml",
267
323
  ],
268
324
  )
325
+ if is_missing_service_principal_error(combined):
326
+ tenant = tenant_id or missing_service_principal_tenant(combined) or ""
327
+ consent_hint = "dxs auth consent" + (f" --tenant-id {tenant}" if tenant else "")
328
+ return AuthenticationError(
329
+ message=render_missing_service_principal_notice(tenant, domain),
330
+ code="DXS-AUTH-030",
331
+ suggestions=[
332
+ "dxs init --domain wavelength<org>.onmicrosoft.com "
333
+ "# sign in against the tenant Datex provisioned for you",
334
+ "Sign in there with the same work email — the tenant only decides which "
335
+ "directory issues the token, not which credentials you type",
336
+ "Ask Datex support which Entra tenant your organization signs in against",
337
+ "If your organization does sign in with its own tenant, a tenant admin "
338
+ f"must grant consent first: {consent_hint}",
339
+ ],
340
+ )
269
341
  if not external:
270
342
  return error
271
343
  if upn and is_guest_account_error(combined):
@@ -286,6 +358,58 @@ def map_login_failure(
286
358
  return error
287
359
 
288
360
 
361
+ def apply_sign_in_tenant_hint(
362
+ error: "AuthenticationError",
363
+ *,
364
+ requested_tenant: str,
365
+ sign_in: "SignInTenant | None",
366
+ ) -> "AuthenticationError":
367
+ """Replace the generic AADSTS650052 advice with this org's actual tenant.
368
+
369
+ :func:`map_login_failure` can only describe the *shape* of the fix, since
370
+ nothing local knows which directory a given organization signs in against.
371
+ Given Quartermaster's answer, the advice collapses to one command — or, when
372
+ the tenant that refused the login is the right one after all, to the consent
373
+ route instead. ``sign_in`` of None (unknown org, service unreachable, older
374
+ service) leaves the generic notice exactly as it was.
375
+ """
376
+ from dxs.utils.errors import AuthenticationError
377
+
378
+ if sign_in is None:
379
+ return error
380
+
381
+ if sign_in.tenant_id.strip().lower() == requested_tenant.strip().lower():
382
+ return AuthenticationError(
383
+ message=(
384
+ f"AADSTS650052: {requested_tenant} is the tenant your organization signs in "
385
+ "against, so the Datex apps have simply never been consented there — no "
386
+ "service principal for them exists in it yet."
387
+ ),
388
+ code="DXS-AUTH-030",
389
+ suggestions=[
390
+ f"A tenant admin runs once: dxs auth consent --tenant-id {requested_tenant}",
391
+ "A blank Microsoft page carrying admin_consent=True is success, not a failure",
392
+ "Then retry the login",
393
+ ],
394
+ )
395
+
396
+ target = sign_in.domain or sign_in.tenant_id
397
+ flag = f"--domain {sign_in.domain}" if sign_in.domain else f"--tenant-id {sign_in.tenant_id}"
398
+ return AuthenticationError(
399
+ message=(
400
+ f"AADSTS650052: your organization signs in against {target}, not tenant "
401
+ f"{requested_tenant} — that one is only where your work-email domain resolves, "
402
+ "and the Datex apps do not exist in it."
403
+ ),
404
+ code="DXS-AUTH-030",
405
+ suggestions=[
406
+ f"dxs init {flag}",
407
+ "Sign in there with the same work email — you are a guest in that directory, so "
408
+ "your own organization's credentials still apply",
409
+ ],
410
+ )
411
+
412
+
289
413
  def browser_allowed(no_browser: bool = False) -> bool:
290
414
  """Whether login chrome may open (or be) a browser.
291
415
 
@@ -0,0 +1,5 @@
1
+ """Quartermaster companion-service client (spec §10, §16)."""
2
+
3
+ from dxs.core.quartermaster.client import QuartermasterClient, QuartermasterError
4
+
5
+ __all__ = ["QuartermasterClient", "QuartermasterError"]
@@ -0,0 +1,190 @@
1
+ """The shared Quartermaster HTTP helper for interactive families (spec §16).
2
+
3
+ House rules it enforces so every family inherits them:
4
+
5
+ - ``User-Agent: dxs-cli/{__version__}`` on every request — the per-version
6
+ traffic view the sunset rule reads.
7
+ - A bare 404 on a family route means the service predates the CLI: mapped to
8
+ a plain "doesn't serve {family}/{segment}" error. A 404 *with* the
9
+ structured ``{"error": {...}}`` body is the family's own answer and is
10
+ surfaced as-is.
11
+ - Tolerant reader: unknown response fields are ignored; genuine failures
12
+ surface the service's machine-readable code — fail loud, never guess.
13
+
14
+ Telemetry does not use this helper (fire-and-forget, frozen sender).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from typing import Any
21
+
22
+ import httpx
23
+
24
+ from dxs import __version__
25
+ from dxs.core.auth.decorators import get_access_token
26
+ from dxs.utils.config import get_settings
27
+ from dxs.utils.errors import AuthenticationError, DxsError
28
+
29
+ _FAMILY_ROUTE = re.compile(r"^/(?P<family>[a-z][a-z0-9_-]*)/(?P<segment>v\d+)(?:/|$)")
30
+
31
+
32
+ class QuartermasterError(DxsError):
33
+ """A Quartermaster request failed; ``service_code`` carries the structured code, if any."""
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ code: str = "DXS-QM-003",
39
+ *,
40
+ service_code: str | None = None,
41
+ status_code: int | None = None,
42
+ suggestions: list[str] | None = None,
43
+ details: dict[str, Any] | None = None,
44
+ ) -> None:
45
+ details = dict(details or {})
46
+ if service_code is not None:
47
+ details["service_code"] = service_code
48
+ if status_code is not None:
49
+ details["status_code"] = status_code
50
+ super().__init__(code=code, message=message, details=details, suggestions=suggestions)
51
+ self.service_code = service_code
52
+ self.status_code = status_code
53
+
54
+
55
+ def _family_and_segment(path: str) -> str:
56
+ match = _FAMILY_ROUTE.match(path)
57
+ if match is None:
58
+ return path
59
+ return f"{match.group('family')}/{match.group('segment')}"
60
+
61
+
62
+ def _structured_error(response: httpx.Response) -> dict[str, Any] | None:
63
+ try:
64
+ body = response.json()
65
+ except ValueError:
66
+ return None
67
+ error = body.get("error") if isinstance(body, dict) else None
68
+ return error if isinstance(error, dict) else None
69
+
70
+
71
+ class QuartermasterClient:
72
+ def __init__(self, base_url: str | None = None, timeout: float = 30.0) -> None:
73
+ resolved = base_url if base_url is not None else get_settings().quartermaster_base_url
74
+ if not resolved:
75
+ raise QuartermasterError(
76
+ "Quartermaster base URL is not configured",
77
+ code="DXS-QM-001",
78
+ suggestions=[
79
+ "Set DXS_QUARTERMASTER_BASE_URL, or quartermaster_base_url in "
80
+ "~/.datex/config.yaml",
81
+ ],
82
+ )
83
+ self.base_url = resolved.rstrip("/")
84
+ self._timeout = timeout
85
+ self._client: httpx.Client | None = None
86
+
87
+ # -- lifecycle -------------------------------------------------------
88
+
89
+ def _http(self) -> httpx.Client:
90
+ if self._client is None:
91
+ self._client = httpx.Client(
92
+ base_url=self.base_url,
93
+ timeout=self._timeout,
94
+ verify=get_settings().verify_ssl,
95
+ headers={"User-Agent": f"dxs-cli/{__version__}"},
96
+ )
97
+ return self._client
98
+
99
+ def close(self) -> None:
100
+ if self._client is not None:
101
+ self._client.close()
102
+ self._client = None
103
+
104
+ def __enter__(self) -> QuartermasterClient:
105
+ return self
106
+
107
+ def __exit__(self, *_: Any) -> None:
108
+ self.close()
109
+
110
+ # -- requests --------------------------------------------------------
111
+
112
+ def _request(
113
+ self, path: str, params: dict[str, Any] | None, accept: str, *, anonymous: bool = False
114
+ ) -> httpx.Response:
115
+ """``anonymous`` omits the bearer entirely — not merely optional.
116
+
117
+ The bootstrap route (`/tenants/v1/signin-tenant`) is reached while a
118
+ login is failing, so there is no token to attach and asking for one
119
+ would raise where the caller needs an answer or a shrug.
120
+ """
121
+ headers = {"Accept": accept}
122
+ if not anonymous:
123
+ headers["Authorization"] = f"Bearer {get_access_token()}"
124
+ try:
125
+ response = self._http().get(path, params=params, headers=headers)
126
+ except httpx.HTTPError as exc:
127
+ raise QuartermasterError(
128
+ f"Could not reach Quartermaster at {self.base_url}: {exc}",
129
+ code="DXS-QM-004",
130
+ suggestions=[
131
+ "Check your network connection",
132
+ "If you point at a staging or local instance, verify "
133
+ "DXS_QUARTERMASTER_BASE_URL",
134
+ ],
135
+ ) from exc
136
+ self._raise_for_status(path, response)
137
+ return response
138
+
139
+ def _raise_for_status(self, path: str, response: httpx.Response) -> None:
140
+ if response.is_success:
141
+ return
142
+ status = response.status_code
143
+ if status == 401:
144
+ raise AuthenticationError(
145
+ message="Quartermaster rejected the access token",
146
+ code="DXS-AUTH-001",
147
+ suggestions=["Run 'dxs auth login' to refresh your session"],
148
+ )
149
+ error = _structured_error(response)
150
+ if error is not None:
151
+ message = str(error.get("message") or f"Quartermaster returned {status}")
152
+ extra = {k: v for k, v in error.items() if k not in ("code", "message")}
153
+ raise QuartermasterError(
154
+ message,
155
+ service_code=str(error.get("code")) if error.get("code") else None,
156
+ status_code=status,
157
+ details=extra or None,
158
+ )
159
+ if status == 404:
160
+ route = _family_and_segment(path)
161
+ raise QuartermasterError(
162
+ f"Quartermaster at {self.base_url} doesn't serve {route} — "
163
+ "the service may be older than your CLI",
164
+ code="DXS-QM-002",
165
+ status_code=status,
166
+ suggestions=[
167
+ "Check DXS_QUARTERMASTER_BASE_URL points at a current Quartermaster",
168
+ ],
169
+ )
170
+ raise QuartermasterError(
171
+ f"Quartermaster returned HTTP {status} for {path}",
172
+ status_code=status,
173
+ )
174
+
175
+ def get_json(
176
+ self, path: str, params: dict[str, Any] | None = None, *, anonymous: bool = False
177
+ ) -> Any:
178
+ response = self._request(path, params, "application/json", anonymous=anonymous)
179
+ try:
180
+ return response.json()
181
+ except ValueError as exc:
182
+ raise QuartermasterError(
183
+ f"Quartermaster returned a non-JSON body for {path}", status_code=200
184
+ ) from exc
185
+
186
+ def get_bytes(
187
+ self, path: str, params: dict[str, Any] | None = None
188
+ ) -> tuple[bytes, dict[str, str]]:
189
+ response = self._request(path, params, "application/gzip, application/json")
190
+ return response.content, {k.lower(): v for k, v in response.headers.items()}
@@ -0,0 +1,57 @@
1
+ """Which Entra tenant an organization signs in against (`/tenants/v1/`).
2
+
3
+ The bootstrap half of the AADSTS650052 recovery: a work-email domain resolves,
4
+ through Entra's public discovery document, to the caller's own corporate
5
+ tenant — which for every org Datex provisioned a `wavelength<org>.onmicrosoft.com`
6
+ tenant for is the wrong directory. Quartermaster holds the org records that map
7
+ one to the other and answers anonymously, which is what makes it reachable from
8
+ a login that has just failed.
9
+
10
+ Every failure is a shrug. This runs on an error path, after a login already
11
+ failed with a diagnosis in hand: a Quartermaster that is down, slow, blocked by
12
+ a corporate proxy, older than this CLI, or simply not configured must leave that
13
+ diagnosis alone rather than replace it with a worse one.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+
20
+ from dxs.core.quartermaster.client import QuartermasterClient
21
+
22
+ # Short on purpose: a failed login is already waiting on this.
23
+ _TIMEOUT_SECONDS = 5.0
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class SignInTenant:
28
+ """The directory an org authenticates against; ``domain`` when one is recorded."""
29
+
30
+ tenant_id: str
31
+ domain: str | None
32
+
33
+
34
+ def lookup_sign_in_tenant(tenant_id: str) -> SignInTenant | None:
35
+ """Resolve ``tenant_id`` to the tenant its organization signs in against.
36
+
37
+ Returns None for every failure and for an unrecognized tenant — callers use
38
+ it to sharpen an error, never to decide whether one occurred.
39
+ """
40
+ try:
41
+ with QuartermasterClient(timeout=_TIMEOUT_SECONDS) as client:
42
+ body = client.get_json(
43
+ "/tenants/v1/signin-tenant", {"tenant": tenant_id}, anonymous=True
44
+ )
45
+ except Exception: # noqa: BLE001 - best-effort by contract; see module docstring
46
+ return None
47
+
48
+ if not isinstance(body, dict):
49
+ return None
50
+ resolved = body.get("sign_in_tenant_id")
51
+ if not isinstance(resolved, str) or not resolved.strip():
52
+ return None
53
+ domain = body.get("sign_in_domain")
54
+ return SignInTenant(
55
+ tenant_id=resolved.strip(),
56
+ domain=domain.strip() if isinstance(domain, str) and domain.strip() else None,
57
+ )
@@ -0,0 +1 @@
1
+ """Skill management (``dxs skills``, issue #95): layout, lockfile, installer, resolver."""
@@ -0,0 +1,30 @@
1
+ """Read a skill's frontmatter from an installed copy (offline ``remove``/``doctor``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+
11
+ def parse_frontmatter(text: str) -> dict[str, Any]:
12
+ if not text.startswith("---"):
13
+ return {}
14
+ end = text.find("\n---", 3)
15
+ if end == -1:
16
+ return {}
17
+ try:
18
+ doc = yaml.safe_load(text[3:end])
19
+ except yaml.YAMLError:
20
+ return {}
21
+ return doc if isinstance(doc, dict) else {}
22
+
23
+
24
+ def installed_depends(skill_dir: Path) -> list[str]:
25
+ skill_md = skill_dir / "SKILL.md"
26
+ if not skill_md.is_file():
27
+ return []
28
+ meta = parse_frontmatter(skill_md.read_text(encoding="utf-8", errors="replace"))
29
+ raw = meta.get("depends") or []
30
+ return [str(d) for d in raw] if isinstance(raw, list) else []