datex-studio-cli 0.4.8__tar.gz → 0.4.10__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 (171) hide show
  1. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/PKG-INFO +1 -1
  2. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/pyproject.toml +1 -1
  3. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/__init__.py +1 -1
  4. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/auth.py +51 -4
  5. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/source.py +186 -109
  6. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/studio.py +85 -19
  7. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/__init__.py +2 -0
  8. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/app_config.py +90 -0
  9. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/decorators.py +33 -11
  10. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/msal_client.py +34 -6
  11. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/token_cache.py +204 -59
  12. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/client.py +5 -0
  13. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/release_notes.py +196 -1
  14. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/paths.py +33 -11
  15. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/app.py +3 -0
  16. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/.gitignore +0 -0
  17. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/README.md +0 -0
  18. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/report-creator.skill +0 -0
  19. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/scripts/analyze_corpus.py +0 -0
  20. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/scripts/generate_rdlx_schema.py +0 -0
  21. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/__main__.py +0 -0
  22. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/cli.py +0 -0
  23. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/__init__.py +0 -0
  24. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/api.py +0 -0
  25. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/branch.py +0 -0
  26. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/config.py +0 -0
  27. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/configuration.py +0 -0
  28. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/crm.py +0 -0
  29. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/datasource.py +0 -0
  30. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/devops.py +0 -0
  31. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/document.py +0 -0
  32. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/endpoint.py +0 -0
  33. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/env.py +0 -0
  34. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/explore.py +0 -0
  35. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/function.py +0 -0
  36. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/marketplace.py +0 -0
  37. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/odata.py +0 -0
  38. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/organization.py +0 -0
  39. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/proxy.py +0 -0
  40. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/release_notes.py +0 -0
  41. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/repo.py +0 -0
  42. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/__init__.py +0 -0
  43. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/add_cmds.py +0 -0
  44. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/api.py +0 -0
  45. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/batch.py +0 -0
  46. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/create.py +0 -0
  47. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/data.py +0 -0
  48. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/dataset.py +0 -0
  49. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/edit.py +0 -0
  50. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/folder.py +0 -0
  51. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/preview.py +0 -0
  52. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/schema_cmds.py +0 -0
  53. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/schema.py +0 -0
  54. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/servicepack.py +0 -0
  55. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/telemetry.py +0 -0
  56. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/user.py +0 -0
  57. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/context.py +0 -0
  58. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/__init__.py +0 -0
  59. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/client.py +0 -0
  60. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/endpoints.py +0 -0
  61. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/metadata_models.py +0 -0
  62. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/models.py +0 -0
  63. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/__init__.py +0 -0
  64. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/cache.py +0 -0
  65. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/__init__.py +0 -0
  66. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/flow_generator.py +0 -0
  67. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/flow_models.py +0 -0
  68. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/generator.py +0 -0
  69. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/models.py +0 -0
  70. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/parsers.py +0 -0
  71. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/resolver.py +0 -0
  72. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/type_def_parser.py +0 -0
  73. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/validator.py +0 -0
  74. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/__init__.py +0 -0
  75. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/flow_models.py +0 -0
  76. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/models.py +0 -0
  77. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/parsers.py +0 -0
  78. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/__init__.py +0 -0
  79. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/client.py +0 -0
  80. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/helpers.py +0 -0
  81. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/models.py +0 -0
  82. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/__init__.py +0 -0
  83. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/client.py +0 -0
  84. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  85. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/__init__.py +0 -0
  86. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/edmx_parser.py +0 -0
  87. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/metadata.py +0 -0
  88. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/__init__.py +0 -0
  89. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/generator.py +0 -0
  90. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/models.py +0 -0
  91. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/validator.py +0 -0
  92. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/graph.py +0 -0
  93. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/__init__.py +0 -0
  94. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/csv_fmt.py +0 -0
  95. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/formatter.py +0 -0
  96. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/json_fmt.py +0 -0
  97. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/redaction.py +0 -0
  98. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/yaml_fmt.py +0 -0
  99. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/responses.py +0 -0
  100. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/__init__.py +0 -0
  101. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/config.py +0 -0
  102. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/identity.py +0 -0
  103. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/recorder.py +0 -0
  104. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/scrubber.py +0 -0
  105. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/sender.py +0 -0
  106. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/models/__init__.py +0 -0
  107. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/__init__.py +0 -0
  108. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/datasource_binding.py +0 -0
  109. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/engine.py +0 -0
  110. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/field_parser.py +0 -0
  111. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/folder.py +0 -0
  112. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/manifest.py +0 -0
  113. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/models.py +0 -0
  114. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/preview.py +0 -0
  115. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/preview_arjs.py +0 -0
  116. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/schema.py +0 -0
  117. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/svg_renderer.py +0 -0
  118. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/templates/__init__.py +0 -0
  119. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  120. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  121. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/validate_arjs.py +0 -0
  122. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/validator.py +0 -0
  123. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/wrapper.py +0 -0
  124. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/__init__.py +0 -0
  125. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/click_options.py +0 -0
  126. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/config.py +0 -0
  127. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/errors.py +0 -0
  128. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/filtering.py +0 -0
  129. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/image.py +0 -0
  130. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/resolvers.py +0 -0
  131. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/responses.py +0 -0
  132. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/restricted.py +0 -0
  133. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/sorting.py +0 -0
  134. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/update_check.py +0 -0
  135. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/__init__.py +0 -0
  136. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/design.py +0 -0
  137. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/proxy_app.py +0 -0
  138. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/404/index.html +0 -0
  139. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/404.html +0 -0
  140. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
  141. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
  142. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  143. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  144. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  145. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
  146. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  147. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  148. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  149. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  150. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  151. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
  152. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
  153. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
  154. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
  155. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  156. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  157. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  158. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  159. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  160. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  161. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  162. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  163. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/capture/index.html +0 -0
  164. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/capture/index.txt +0 -0
  165. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/index.html +0 -0
  166. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/index.txt +0 -0
  167. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/index.html +0 -0
  168. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/index.txt +0 -0
  169. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/render-cli.js +0 -0
  170. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/report-preview.html +0 -0
  171. {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/report-validate.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datex-studio-cli
3
- Version: 0.4.8
3
+ Version: 0.4.10
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.8"
7
+ version = "0.4.10"
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.8"
3
+ __version__ = "0.4.10"
4
4
  __app_name__ = "dxs"
@@ -14,6 +14,44 @@ from dxs.utils.responses import list_response, single
14
14
  from dxs.utils.restricted import restrict_in_restricted_mode
15
15
 
16
16
 
17
+ def _format_age(seconds: int) -> str:
18
+ """Human-readable age from a number of seconds."""
19
+ if seconds < 60:
20
+ return f"{seconds} seconds"
21
+ if seconds < 3600:
22
+ return f"{seconds // 60} minutes"
23
+ if seconds < 86400:
24
+ return f"{seconds // 3600} hours"
25
+ return f"{seconds // 86400} days"
26
+
27
+
28
+ def _refresh_token_info(identity: Any, now: datetime) -> dict[str, Any]:
29
+ """Describe the identity's refresh token: presence and age.
30
+
31
+ Azure does not return a refresh-token expiry, so we surface the age of the
32
+ stored token (recorded when it was obtained) to help diagnose refresh issues.
33
+ """
34
+ if not identity.refresh_token:
35
+ return {
36
+ "present": False,
37
+ "note": "No refresh token stored; a full login is required.",
38
+ }
39
+
40
+ info: dict[str, Any] = {
41
+ "present": True,
42
+ "note": "Azure does not expose refresh-token expiry; age shown for reference.",
43
+ }
44
+ acquired_at = identity.refresh_token_acquired_at
45
+ if acquired_at is not None:
46
+ age_seconds = int((now - acquired_at).total_seconds())
47
+ info["acquired_at"] = acquired_at.isoformat()
48
+ info["age_human"] = _format_age(max(age_seconds, 0))
49
+ else:
50
+ info["acquired_at"] = None
51
+ info["age_human"] = "unknown (log in again to start tracking age)"
52
+ return info
53
+
54
+
17
55
  @click.group()
18
56
  def auth() -> None:
19
57
  """Authentication commands.
@@ -398,8 +436,8 @@ def status(ctx: DxsContext) -> None:
398
436
  # If primary token is expired, try to refresh it
399
437
  if is_expired:
400
438
  ctx.log("Primary access token is expired, attempting to refresh...")
401
- refreshed_token = cache.try_refresh()
402
- if refreshed_token:
439
+ outcome = cache.try_refresh_detailed()
440
+ if outcome.access_token:
403
441
  ctx.log("Successfully refreshed access token")
404
442
  refreshed_identity = cache.get_active_identity()
405
443
  if refreshed_identity:
@@ -415,6 +453,9 @@ def status(ctx: DxsContext) -> None:
415
453
  "message": "Authentication token has expired and refresh failed",
416
454
  "account": active_identity.account_username,
417
455
  "expired_at": active_identity.expires_at.isoformat(),
456
+ "refresh_error": outcome.error,
457
+ "refresh_error_description": outcome.error_description,
458
+ "refresh_token": _refresh_token_info(active_identity, now),
418
459
  "suggestion": "Run 'dxs auth login' to re-authenticate",
419
460
  },
420
461
  semantic_key="authentication",
@@ -430,9 +471,14 @@ def status(ctx: DxsContext) -> None:
430
471
  else:
431
472
  return f"{seconds // 3600} hours"
432
473
 
433
- # Check all cached tokens for each resource
474
+ # Check all cached tokens for each resource.
475
+ # External-tenant identities must be probed against their own tenant authority;
476
+ # redeeming their refresh token against the default tenant returns invalid_grant.
434
477
  settings = get_settings()
435
- client = MSALClient()
478
+ if active_identity.external_entra_id and active_identity.tenant_id:
479
+ client = MSALClient.for_tenant(active_identity.tenant_id)
480
+ else:
481
+ client = MSALClient()
436
482
 
437
483
  resources = {
438
484
  "datex_api": {
@@ -525,6 +571,7 @@ def status(ctx: DxsContext) -> None:
525
571
  "expires_in_seconds": expires_in_seconds,
526
572
  "expires_in_human": format_expiration(expires_in_seconds),
527
573
  },
574
+ "refresh_token": _refresh_token_info(active_identity, now),
528
575
  },
529
576
  "tokens": tokens_info if tokens_info else None,
530
577
  }
@@ -22,6 +22,7 @@ from dxs.core.api import (
22
22
  RepoEndpoints,
23
23
  SourceControlEndpoints,
24
24
  WorkitemEndpoints,
25
+ diff_references,
25
26
  fetch_references,
26
27
  fetch_settings,
27
28
  )
@@ -29,10 +30,13 @@ from dxs.core.auth import require_auth
29
30
  from dxs.core.output.yaml_fmt import LiteralString
30
31
  from dxs.core.release_notes import (
31
32
  aggregate_contributors,
33
+ build_repo_index,
32
34
  classify_dependencies,
33
35
  collect_workitems_batch,
34
36
  enrich_workitems_with_devops_data,
35
37
  is_sync_commit,
38
+ resolve_changed_dependencies,
39
+ resolve_root_org_ids,
36
40
  )
37
41
  from dxs.utils.errors import ValidationError
38
42
  from dxs.utils.responses import list_response, single
@@ -2770,106 +2774,186 @@ def deps_diff(ctx: DxsContext, from_branch: int, to_branch: int) -> None:
2770
2774
  """
2771
2775
  client = ApiClient()
2772
2776
 
2773
- # Fetch dependencies for both branches in parallel conceptually
2774
2777
  ctx.log(f"Fetching dependencies for branch {from_branch}...")
2775
2778
  from_deps = fetch_references(client, from_branch, direct_only=True)
2776
2779
 
2777
2780
  ctx.log(f"Fetching dependencies for branch {to_branch}...")
2778
2781
  to_deps = fetch_references(client, to_branch, direct_only=True)
2779
2782
 
2780
- # Normalize to lists
2781
- if not isinstance(from_deps, list):
2782
- from_deps = [from_deps] if from_deps else []
2783
- if not isinstance(to_deps, list):
2784
- to_deps = [to_deps] if to_deps else []
2785
-
2786
- # Build lookup maps by referenceName
2787
- from_by_ref = {d.get("referenceName"): d for d in from_deps}
2788
- to_by_ref = {d.get("referenceName"): d for d in to_deps}
2789
-
2790
- # Compute differences
2791
- added = []
2792
- removed = []
2793
- updated = []
2794
- unchanged_count = 0
2795
-
2796
- # Find added and updated
2797
- for ref_name, to_dep in to_by_ref.items():
2798
- if ref_name not in from_by_ref:
2799
- # New dependency
2800
- added.append(
2801
- {
2802
- "referenceName": ref_name,
2803
- "name": to_dep.get("name"),
2804
- "description": to_dep.get("description"),
2805
- "versionName": to_dep.get("versionName"),
2806
- "versionId": to_dep.get("marketPlaceApplicationVersionId"),
2807
- "applicationId": to_dep.get("applicationId"),
2808
- "commitTitle": to_dep.get("commitTitle"),
2809
- "isDirect": to_dep.get("isDirect"),
2810
- }
2811
- )
2812
- else:
2813
- from_dep = from_by_ref[ref_name]
2814
- from_version_id = from_dep.get("marketPlaceApplicationVersionId")
2815
- to_version_id = to_dep.get("marketPlaceApplicationVersionId")
2783
+ # Diff by resolved `version` keyed on uniqueIdentifier. (The legacy
2784
+ # marketPlaceApplicationVersionId field no longer exists in the AppConfig
2785
+ # payload, so the previous comparison reported everything as unchanged.)
2786
+ diff = diff_references(from_deps, to_deps)
2816
2787
 
2817
- if from_version_id != to_version_id:
2818
- # Updated dependency
2819
- updated.append(
2820
- {
2821
- "referenceName": ref_name,
2822
- "name": to_dep.get("name"),
2823
- "fromVersionId": from_version_id,
2824
- "toVersionId": to_version_id,
2825
- "fromApplicationId": from_dep.get("applicationId"),
2826
- "toApplicationId": to_dep.get("applicationId"),
2827
- "fromVersionName": from_dep.get("versionName"),
2828
- "toVersionName": to_dep.get("versionName"),
2829
- "fromCommitTitle": from_dep.get("commitTitle"),
2830
- "toCommitTitle": to_dep.get("commitTitle"),
2831
- "isDirect": to_dep.get("isDirect"),
2832
- }
2833
- )
2834
- else:
2835
- unchanged_count += 1
2788
+ ctx.output(
2789
+ single(
2790
+ item=diff,
2791
+ semantic_key="dependency_changes",
2792
+ from_branch_id=from_branch,
2793
+ to_branch_id=to_branch,
2794
+ )
2795
+ )
2836
2796
 
2837
- # Find removed
2838
- for ref_name, from_dep in from_by_ref.items():
2839
- if ref_name not in to_by_ref:
2840
- removed.append(
2841
- {
2842
- "referenceName": ref_name,
2843
- "name": from_dep.get("name"),
2844
- "description": from_dep.get("description"),
2845
- "versionName": from_dep.get("versionName"),
2846
- "versionId": from_dep.get("marketPlaceApplicationVersionId"),
2847
- "applicationId": from_dep.get("applicationId"),
2848
- "isDirect": from_dep.get("isDirect"),
2849
- }
2797
+
2798
+ @source.command("release-tree")
2799
+ @click.option(
2800
+ "--from",
2801
+ "from_branch",
2802
+ type=int,
2803
+ required=True,
2804
+ help="Source branch ID (earlier release of the root application)",
2805
+ )
2806
+ @click.option(
2807
+ "--to",
2808
+ "to_branch",
2809
+ type=int,
2810
+ required=True,
2811
+ help="Target branch ID (later release of the root application)",
2812
+ )
2813
+ @click.option(
2814
+ "--recursive",
2815
+ is_flag=True,
2816
+ default=False,
2817
+ help="Also walk changed dependencies of changed dependencies (transitive)",
2818
+ )
2819
+ @click.option(
2820
+ "--max-depth",
2821
+ type=int,
2822
+ default=10,
2823
+ help="Maximum recursion depth when --recursive is set (default: 10)",
2824
+ )
2825
+ @pass_context
2826
+ @require_auth
2827
+ def release_tree(
2828
+ ctx: DxsContext,
2829
+ from_branch: int,
2830
+ to_branch: int,
2831
+ recursive: bool,
2832
+ max_depth: int,
2833
+ ) -> None:
2834
+ """Build the complete changed-dependency worklist between two releases.
2835
+
2836
+ Diffs the root application's dependencies by resolved ``version``, then
2837
+ resolves every changed dependency to its from/to branch IDs (the IDs needed
2838
+ to ``compare`` or drill each package). This is the enumeration step for
2839
+ release notes: one call yields the full list of (package, from_branch,
2840
+ to_branch) tuples to analyze.
2841
+
2842
+ Unlike ``deps-diff`` (which lists version deltas only), this resolves branch
2843
+ IDs. With ``--recursive`` it walks changed dependencies of changed
2844
+ dependencies; for a flat aggregator app (e.g. FootprintManager, which
2845
+ declares every package directly) a single pass is already complete.
2846
+
2847
+ \b
2848
+ Options:
2849
+ --from ID Earlier release branch ID of the root app
2850
+ --to ID Later release branch ID of the root app
2851
+ --recursive Walk transitive changed dependencies
2852
+ --max-depth N Recursion cap with --recursive (default: 10)
2853
+
2854
+ \b
2855
+ Examples:
2856
+ dxs source release-tree --from 73496 --to 82962
2857
+ dxs source release-tree --from 73496 --to 82962 --recursive
2858
+ """
2859
+ if from_branch == to_branch:
2860
+ raise ValidationError(
2861
+ message="Cannot compare a branch to itself",
2862
+ code="DXS-VAL-002",
2863
+ suggestions=["Provide different --from and --to branch IDs"],
2864
+ )
2865
+
2866
+ client = ApiClient()
2867
+
2868
+ # Root metadata (reference name + version names for the worklist header).
2869
+ from_data = client.get(BranchEndpoints.get(from_branch))
2870
+ to_data = client.get(BranchEndpoints.get(to_branch))
2871
+
2872
+ # The root may be a customer application (e.g. tob-footprint) that references
2873
+ # packages from its own org alongside the shared Datex catalog. Index that
2874
+ # org too, or those packages (e.g. tob-reports) silently fail to resolve.
2875
+ extra_org_ids = resolve_root_org_ids(client, to_data, from_data)
2876
+ root_name = to_data.get("referenceName") or to_data.get("name")
2877
+
2878
+ root = {
2879
+ "reference_name": root_name,
2880
+ "from_version": from_data.get("versionName"),
2881
+ "to_version": to_data.get("versionName"),
2882
+ "from_branch_id": from_branch,
2883
+ "to_branch_id": to_branch,
2884
+ "depth": 0,
2885
+ "is_root": True,
2886
+ }
2887
+
2888
+ # Repo index is built once and reused across all levels.
2889
+ ctx.log("Building repository index...")
2890
+ repo_index = build_repo_index(client, extra_org_ids=extra_org_ids)
2891
+
2892
+ dependencies: list[dict[str, Any]] = []
2893
+ added: list[dict[str, Any]] = []
2894
+ removed: list[dict[str, Any]] = []
2895
+ # visited keyed by the work unit, so the same (package, from, to) is resolved once.
2896
+ visited: set[tuple[Any, Any, Any]] = set()
2897
+
2898
+ # BFS queue of (from_branch_id, to_branch_id, depth) to diff.
2899
+ queue: list[tuple[int, int, int]] = [(from_branch, to_branch, 1)]
2900
+ while queue:
2901
+ cur_from, cur_to, depth = queue.pop(0)
2902
+ ctx.log(f"Diffing dependencies at depth {depth} ({cur_from} -> {cur_to})...")
2903
+ try:
2904
+ from_refs = fetch_references(client, cur_from, direct_only=True)
2905
+ to_refs = fetch_references(client, cur_to, direct_only=True)
2906
+ except Exception:
2907
+ logger.debug("Failed to fetch references for %s/%s", cur_from, cur_to, exc_info=True)
2908
+ continue
2909
+
2910
+ diff = diff_references(from_refs, to_refs)
2911
+ if depth == 1:
2912
+ added = diff["added"]
2913
+ removed = diff["removed"]
2914
+
2915
+ resolved = resolve_changed_dependencies(client, diff["updated"], repo_index=repo_index)
2916
+ for entry in resolved:
2917
+ key = (
2918
+ entry.get("unique_identifier"),
2919
+ entry.get("from_version"),
2920
+ entry.get("to_version"),
2850
2921
  )
2922
+ if key in visited:
2923
+ continue
2924
+ visited.add(key)
2925
+ entry["depth"] = depth
2926
+ dependencies.append(entry)
2927
+
2928
+ if (
2929
+ recursive
2930
+ and depth < max_depth
2931
+ and entry.get("from_branch_id")
2932
+ and entry.get("to_branch_id")
2933
+ ):
2934
+ queue.append((entry["from_branch_id"], entry["to_branch_id"], depth + 1))
2851
2935
 
2852
- # Sort results by referenceName for consistent output
2853
- added.sort(key=lambda x: str(x.get("referenceName") or ""))
2854
- removed.sort(key=lambda x: str(x.get("referenceName") or ""))
2855
- updated.sort(key=lambda x: str(x.get("referenceName") or ""))
2936
+ dependencies.sort(key=lambda d: (d.get("depth", 0), str(d.get("reference_name") or "")))
2937
+ resolved_ok = sum(1 for d in dependencies if not d.get("resolve_error"))
2856
2938
 
2857
2939
  ctx.output(
2858
2940
  single(
2859
2941
  item={
2942
+ "root": root,
2860
2943
  "summary": {
2944
+ "dependencies_total": len(dependencies),
2945
+ "resolved_ok": resolved_ok,
2946
+ "resolve_failed": len(dependencies) - resolved_ok,
2861
2947
  "added_count": len(added),
2862
2948
  "removed_count": len(removed),
2863
- "updated_count": len(updated),
2864
- "unchanged_count": unchanged_count,
2865
- "from_total": len(from_deps),
2866
- "to_total": len(to_deps),
2949
+ "max_depth_reached": max((d.get("depth", 0) for d in dependencies), default=0),
2950
+ "recursive": recursive,
2867
2951
  },
2952
+ "dependencies": dependencies,
2868
2953
  "added": added,
2869
2954
  "removed": removed,
2870
- "updated": updated,
2871
2955
  },
2872
- semantic_key="dependency_changes",
2956
+ semantic_key="release_tree",
2873
2957
  from_branch_id=from_branch,
2874
2958
  to_branch_id=to_branch,
2875
2959
  )
@@ -3661,54 +3745,47 @@ def compare(
3661
3745
  filtered.append(entry)
3662
3746
  committed_branches_list = filtered
3663
3747
 
3664
- # Get dependency changes
3748
+ # Get dependency changes. Diff by resolved `version` keyed on uniqueIdentifier
3749
+ # (the legacy marketPlaceApplicationVersionId field is gone from the AppConfig
3750
+ # payload, which is why the old comparison always reported zero changes).
3665
3751
  dep_changes: dict[str, list[Any]] = {"added": [], "removed": [], "updated": []}
3666
3752
  ctx.log("Comparing dependencies...")
3667
3753
  try:
3668
3754
  from_deps = fetch_references(client, from_branch, direct_only=True)
3669
3755
  to_deps = fetch_references(client, to_branch, direct_only=True)
3670
-
3671
- # Build lookup by reference name
3672
- from_by_ref = {d.get("referenceName"): d for d in from_deps}
3673
- to_by_ref = {d.get("referenceName"): d for d in to_deps}
3674
-
3675
- # Find added, removed, and updated
3676
- for ref, dep in to_by_ref.items():
3677
- if ref not in from_by_ref:
3678
- dep_changes["added"].append(dep)
3679
- else:
3680
- from_dep = from_by_ref[ref]
3681
- from_dep_ver = from_dep.get("marketPlaceApplicationVersionId")
3682
- to_dep_ver = dep.get("marketPlaceApplicationVersionId")
3683
- if from_dep_ver != to_dep_ver:
3684
- dep_changes["updated"].append(
3685
- {
3686
- "reference_name": ref,
3687
- "from_branch_id": from_dep.get("applicationId"),
3688
- "to_branch_id": dep.get("applicationId"),
3689
- }
3690
- )
3691
-
3692
- for ref, dep in from_by_ref.items():
3693
- if ref not in to_by_ref:
3694
- dep_changes["removed"].append(dep)
3756
+ diff = diff_references(from_deps, to_deps)
3757
+ dep_changes["added"] = diff["added"]
3758
+ dep_changes["removed"] = diff["removed"]
3759
+ dep_changes["updated"] = diff["updated"]
3695
3760
  except Exception:
3696
3761
  logger.debug("Failed to fetch dependencies for comparison", exc_info=True)
3697
3762
 
3698
- # Phase 4: Recursive dependency enrichment
3763
+ # Phase 4: Recursive dependency enrichment. The AppConfig payload carries no
3764
+ # branch IDs, so resolve each changed dep's from/to branch IDs before drilling.
3699
3765
  sync_only_dep_count = 0
3700
3766
  if recursive and dep_changes["updated"]:
3767
+ ctx.log("Resolving dependency branch IDs...")
3768
+ # Index the root app's own org too, so a customer app's packages resolve.
3769
+ extra_org_ids = resolve_root_org_ids(client, to_branch_data, from_branch_data)
3770
+ resolved = resolve_changed_dependencies(
3771
+ client, dep_changes["updated"], extra_org_ids=extra_org_ids
3772
+ )
3773
+ drillable = [d for d in resolved if d.get("from_branch_id") and d.get("to_branch_id")]
3774
+ unresolved = [
3775
+ d for d in resolved if not (d.get("from_branch_id") and d.get("to_branch_id"))
3776
+ ]
3777
+
3701
3778
  ctx.log("Recursively comparing updated dependencies...")
3702
- meaningful_deps, sync_only_deps = classify_dependencies(client, dep_changes["updated"])
3779
+ meaningful_deps, sync_only_deps = classify_dependencies(client, drillable)
3703
3780
  sync_only_dep_count = len(sync_only_deps)
3704
3781
 
3705
3782
  if exclude_sync:
3706
3783
  # When both flags are on, separate sync-only deps
3707
- dep_changes["updated"] = meaningful_deps
3784
+ dep_changes["updated"] = meaningful_deps + unresolved
3708
3785
  dep_changes["sync_only"] = sync_only_deps
3709
3786
  else:
3710
3787
  # Keep all but mark them
3711
- dep_changes["updated"] = meaningful_deps + sync_only_deps
3788
+ dep_changes["updated"] = meaningful_deps + sync_only_deps + unresolved
3712
3789
 
3713
3790
  # Phase 2: Collect work item IDs from all committed branches + target branch
3714
3791
  workitem_ids: set[str] = set()
@@ -109,19 +109,41 @@ def _launch_server(port: int, no_browser: bool, dev: bool, replace: bool = False
109
109
  uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning", lifespan="on")
110
110
 
111
111
 
112
- def _require_server() -> dict[str, int]:
113
- """Read the studio lockfile and return server info, or raise an error.
112
+ def _resolve_server_port(probe_port: int) -> int:
113
+ """Return the port of a running studio server, preferring the lockfile.
114
114
 
115
- Returns:
116
- dict with ``port`` and ``pid`` keys.
115
+ Falls back to probing the well-known port so a server started in another
116
+ shell or session is still found even when this process can't read or trust
117
+ the lockfile — e.g. a sandboxed Claude Code agent shell on Windows whose
118
+ PID-liveness check or ``Path.home()`` resolves differently than the user's
119
+ interactive session (issue #38).
117
120
 
118
121
  Raises:
119
- click.ClickException: If no running studio server is found.
122
+ click.ClickException: If no running studio server can be located.
120
123
  """
121
124
  lock = read_studio_lock()
122
- if lock is None:
123
- raise click.ClickException("No studio server running. Start with: dxs studio")
124
- return lock
125
+ if lock is not None:
126
+ return int(lock["port"])
127
+ if _port_in_use(probe_port):
128
+ return probe_port
129
+ raise click.ClickException("No studio server running. Start with: dxs studio")
130
+
131
+
132
+ def _unreachable_error(subcommand: str, port: int, file: str | None, exc: Exception) -> str:
133
+ """Build an actionable error for a server that is found but unreachable.
134
+
135
+ Distinguishes "no server" (handled in :func:`_resolve_server_port`) from
136
+ "found but the request failed" — typically a sandboxed agent shell that
137
+ can't open a loopback connection to the user's server.
138
+ """
139
+ arg = f" {file}" if file else ""
140
+ return (
141
+ f"Found a studio server on port {port} but couldn't reach it: {exc}\n"
142
+ "If Studio is running in your own terminal and this is a sandboxed agent "
143
+ "shell (common on Windows), the agent process may be blocked from the "
144
+ "local server. Run the command in your own session instead:\n"
145
+ f" ! dxs studio {subcommand}{arg}"
146
+ )
125
147
 
126
148
 
127
149
  @click.group(invoke_without_command=True)
@@ -166,18 +188,24 @@ def studio(ctx: click.Context, port: int, no_browser: bool, dev: bool, replace:
166
188
 
167
189
  @studio.command("open")
168
190
  @click.argument("file", type=click.Path())
169
- def studio_open(file: str) -> None:
191
+ @click.option(
192
+ "--port",
193
+ "-p",
194
+ type=int,
195
+ default=5051,
196
+ help="Port to probe when no studio lockfile is present (default: 5051)",
197
+ )
198
+ def studio_open(file: str, port: int) -> None:
170
199
  """Open a report file in the studio design canvas.
171
200
 
172
201
  FILE is the path to a report file (.rdlx-json) to open.
173
202
  """
174
- lock = _require_server()
175
- port = lock["port"]
203
+ server_port = _resolve_server_port(port)
176
204
  resolved = str(Path(file).resolve())
177
205
 
178
206
  try:
179
207
  resp = httpx.post(
180
- f"http://127.0.0.1:{port}/api/design/open",
208
+ f"http://127.0.0.1:{server_port}/api/design/open",
181
209
  json={"path": resolved},
182
210
  timeout=10.0,
183
211
  )
@@ -190,31 +218,69 @@ def studio_open(file: str) -> None:
190
218
  raise click.ClickException(detail)
191
219
  resp.raise_for_status()
192
220
  except httpx.HTTPError as exc:
193
- raise click.ClickException(f"Failed to open file in studio: {exc}") from exc
221
+ raise click.ClickException(_unreachable_error("open", server_port, file, exc)) from exc
194
222
 
195
223
  data = resp.json()
196
224
  file_path = data.get("path", resolved)
197
- click.echo(f"Opened {file_path} in studio (http://127.0.0.1:{port}/design)")
225
+ click.echo(f"Opened {file_path} in studio (http://127.0.0.1:{server_port}/design)")
198
226
 
199
227
 
200
228
  @studio.command("close")
201
- def studio_close() -> None:
229
+ @click.option(
230
+ "--port",
231
+ "-p",
232
+ type=int,
233
+ default=5051,
234
+ help="Port to probe when no studio lockfile is present (default: 5051)",
235
+ )
236
+ def studio_close(port: int) -> None:
202
237
  """Close the current design session in studio."""
203
- lock = _require_server()
204
- port = lock["port"]
238
+ server_port = _resolve_server_port(port)
205
239
 
206
240
  try:
207
241
  resp = httpx.post(
208
- f"http://127.0.0.1:{port}/api/design/close",
242
+ f"http://127.0.0.1:{server_port}/api/design/close",
209
243
  timeout=10.0,
210
244
  )
211
245
  resp.raise_for_status()
212
246
  except httpx.HTTPError as exc:
213
- raise click.ClickException(f"Failed to close design session: {exc}") from exc
247
+ raise click.ClickException(_unreachable_error("close", server_port, None, exc)) from exc
214
248
 
215
249
  click.echo("Design session closed")
216
250
 
217
251
 
252
+ @studio.command("stop")
253
+ @click.option(
254
+ "--port",
255
+ "-p",
256
+ type=int,
257
+ default=5051,
258
+ help="Port to probe when no studio lockfile is present (default: 5051)",
259
+ )
260
+ def studio_stop(port: int) -> None:
261
+ """Stop the studio server tracked by the lockfile.
262
+
263
+ Only terminates a server whose PID this CLI recorded in its own lockfile;
264
+ a process we don't manage is never killed.
265
+ """
266
+ lock = read_studio_lock()
267
+ if lock is None:
268
+ if _port_in_use(port):
269
+ raise click.ClickException(
270
+ f"A server is running on port {port} but it isn't tracked by a "
271
+ "studio lockfile, so it won't be stopped automatically. Stop it "
272
+ "from the terminal that launched it."
273
+ )
274
+ raise click.ClickException("No studio server running.")
275
+
276
+ pid = int(lock["pid"])
277
+ server_port = int(lock["port"])
278
+ _terminate_pid(pid)
279
+ _await_port_free(server_port)
280
+ remove_studio_lock()
281
+ click.echo(f"Stopped studio (pid {pid}) on port {server_port}.")
282
+
283
+
218
284
  @studio.command("status")
219
285
  @click.option(
220
286
  "--port",
@@ -1,6 +1,7 @@
1
1
  """API module for Datex Studio HTTP client and models."""
2
2
 
3
3
  from dxs.core.api.app_config import (
4
+ diff_references,
4
5
  fetch_app_config,
5
6
  fetch_references,
6
7
  fetch_settings,
@@ -43,6 +44,7 @@ from dxs.core.api.models import (
43
44
  __all__ = [
44
45
  "ApiClient",
45
46
  # AppConfig helpers
47
+ "diff_references",
46
48
  "fetch_app_config",
47
49
  "fetch_references",
48
50
  "fetch_settings",