datex-studio-cli 0.4.20__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 (216) hide show
  1. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/PKG-INFO +1 -1
  2. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/pyproject.toml +1 -1
  3. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/__init__.py +1 -1
  4. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/cli.py +110 -10
  5. datex_studio_cli-0.4.21/src/dxs/utils/update_check.py +452 -0
  6. datex_studio_cli-0.4.20/src/dxs/utils/update_check.py +0 -208
  7. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/.gitignore +0 -0
  8. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/CONTEXT.md +0 -0
  9. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/README.md +0 -0
  10. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/.env.example +0 -0
  11. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/Dockerfile +0 -0
  12. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/README.md +0 -0
  13. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/apply-local-cache.js +0 -0
  14. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/collector-config.yaml +0 -0
  15. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/docker-compose.yml +0 -0
  16. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/local-test-settings.json +0 -0
  17. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/otel/remote-settings.json +0 -0
  18. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/report-creator.skill +0 -0
  19. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/scripts/analyze_corpus.py +0 -0
  20. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/scripts/generate_rdlx_schema.py +0 -0
  21. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/__main__.py +0 -0
  22. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/__init__.py +0 -0
  23. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/api.py +0 -0
  24. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/auth.py +0 -0
  25. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/branch.py +0 -0
  26. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/branch_commit_guard.py +0 -0
  27. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/cascade.py +0 -0
  28. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/config.py +0 -0
  29. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/configuration.py +0 -0
  30. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/crm.py +0 -0
  31. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/crm_delivery.py +0 -0
  32. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/crm_time.py +0 -0
  33. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/datasource.py +0 -0
  34. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/devops.py +0 -0
  35. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/document.py +0 -0
  36. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/endpoint.py +0 -0
  37. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/env.py +0 -0
  38. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/explore.py +0 -0
  39. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/function.py +0 -0
  40. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/init.py +0 -0
  41. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/marketplace.py +0 -0
  42. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/__init__.py +0 -0
  43. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/_common.py +0 -0
  44. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/api.py +0 -0
  45. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/create.py +0 -0
  46. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/data.py +0 -0
  47. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/pack.py +0 -0
  48. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/preview.py +0 -0
  49. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/odata.py +0 -0
  50. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/organization.py +0 -0
  51. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/proxy.py +0 -0
  52. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/reference.py +0 -0
  53. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/release_notes.py +0 -0
  54. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/repo.py +0 -0
  55. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/__init__.py +0 -0
  56. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/add_cmds.py +0 -0
  57. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/api.py +0 -0
  58. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/batch.py +0 -0
  59. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/create.py +0 -0
  60. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/data.py +0 -0
  61. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/dataset.py +0 -0
  62. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/edit.py +0 -0
  63. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/folder.py +0 -0
  64. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/preview.py +0 -0
  65. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/report/schema_cmds.py +0 -0
  66. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/reset.py +0 -0
  67. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/schema.py +0 -0
  68. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/servicepack.py +0 -0
  69. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/source.py +0 -0
  70. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/studio.py +0 -0
  71. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/telemetry.py +0 -0
  72. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/user.py +0 -0
  73. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/commands/validation_report.py +0 -0
  74. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/context.py +0 -0
  75. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/__init__.py +0 -0
  76. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/agent_browser.py +0 -0
  77. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/__init__.py +0 -0
  78. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/app_config.py +0 -0
  79. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/client.py +0 -0
  80. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/endpoints.py +0 -0
  81. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/metadata_models.py +0 -0
  82. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/models.py +0 -0
  83. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/api/reference_graph.py +0 -0
  84. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/auth/__init__.py +0 -0
  85. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/auth/datex_tenant.py +0 -0
  86. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/auth/decorators.py +0 -0
  87. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/auth/msal_client.py +0 -0
  88. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/auth/tenants.py +0 -0
  89. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/auth/token_cache.py +0 -0
  90. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/cache.py +0 -0
  91. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/__init__.py +0 -0
  92. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_generator.py +0 -0
  93. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_models.py +0 -0
  94. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_shape.py +0 -0
  95. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/generator.py +0 -0
  96. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/models.py +0 -0
  97. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/parsers.py +0 -0
  98. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/resolver.py +0 -0
  99. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/type_def_parser.py +0 -0
  100. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/validator.py +0 -0
  101. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/designer/__init__.py +0 -0
  102. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/designer/flow_models.py +0 -0
  103. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/designer/models.py +0 -0
  104. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/designer/parsers.py +0 -0
  105. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/devops/__init__.py +0 -0
  106. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/devops/client.py +0 -0
  107. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/devops/helpers.py +0 -0
  108. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/devops/models.py +0 -0
  109. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/__init__.py +0 -0
  110. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/client.py +0 -0
  111. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/html.py +0 -0
  112. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  113. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/pagination.py +0 -0
  114. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/resolvers.py +0 -0
  115. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/time_entry.py +0 -0
  116. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/__init__.py +0 -0
  117. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/client.py +0 -0
  118. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/edmx_parser.py +0 -0
  119. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/metadata.py +0 -0
  120. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/function/__init__.py +0 -0
  121. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/function/generator.py +0 -0
  122. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/function/models.py +0 -0
  123. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/function/validator.py +0 -0
  124. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/graph.py +0 -0
  125. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/onboarding.py +0 -0
  126. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/output/__init__.py +0 -0
  127. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/output/csv_fmt.py +0 -0
  128. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/output/formatter.py +0 -0
  129. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/output/json_fmt.py +0 -0
  130. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/output/redaction.py +0 -0
  131. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/output/yaml_fmt.py +0 -0
  132. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/release_notes.py +0 -0
  133. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/responses.py +0 -0
  134. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/__init__.py +0 -0
  135. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/config.py +0 -0
  136. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/identity.py +0 -0
  137. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/recorder.py +0 -0
  138. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/scrubber.py +0 -0
  139. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/sender.py +0 -0
  140. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/models/__init__.py +0 -0
  141. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/__init__.py +0 -0
  142. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/folder.py +0 -0
  143. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/manifest.py +0 -0
  144. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/mocks.py +0 -0
  145. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/preview.py +0 -0
  146. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/scaffold.py +0 -0
  147. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/ng/serve_lock.py +0 -0
  148. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/py.typed +0 -0
  149. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/__init__.py +0 -0
  150. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/datasource_binding.py +0 -0
  151. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/engine.py +0 -0
  152. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/field_parser.py +0 -0
  153. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/folder.py +0 -0
  154. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/manifest.py +0 -0
  155. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/models.py +0 -0
  156. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/preview.py +0 -0
  157. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/preview_arjs.py +0 -0
  158. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/schema.py +0 -0
  159. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/svg_renderer.py +0 -0
  160. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/templates/__init__.py +0 -0
  161. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  162. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  163. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/validate_arjs.py +0 -0
  164. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/validator.py +0 -0
  165. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/report/wrapper.py +0 -0
  166. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/__init__.py +0 -0
  167. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/click_options.py +0 -0
  168. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/clipboard.py +0 -0
  169. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/config.py +0 -0
  170. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/errors.py +0 -0
  171. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/filtering.py +0 -0
  172. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/image.py +0 -0
  173. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/paths.py +0 -0
  174. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/resolvers.py +0 -0
  175. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/responses.py +0 -0
  176. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/restricted.py +0 -0
  177. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/sorting.py +0 -0
  178. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/utils/versioning.py +0 -0
  179. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/__init__.py +0 -0
  180. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/app.py +0 -0
  181. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/design.py +0 -0
  182. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/proxy_app.py +0 -0
  183. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/404/index.html +0 -0
  184. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/404.html +0 -0
  185. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
  186. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
  187. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  188. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  189. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  190. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
  191. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  192. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  193. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  194. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  195. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  196. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
  197. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
  198. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
  199. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
  200. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  201. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  202. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  203. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  204. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  205. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  206. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  207. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  208. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/capture/index.html +0 -0
  209. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/capture/index.txt +0 -0
  210. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/index.html +0 -0
  211. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/index.txt +0 -0
  212. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/index.html +0 -0
  213. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/index.txt +0 -0
  214. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/render-cli.js +0 -0
  215. {datex_studio_cli-0.4.20 → datex_studio_cli-0.4.21}/src/dxs/web/static/report-preview.html +0 -0
  216. {datex_studio_cli-0.4.20 → 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.20
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.20"
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.20"
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()
@@ -0,0 +1,452 @@
1
+ """PyPI update notifier + self-upgrade machinery.
2
+
3
+ After each command, if the on-disk cache says a newer version exists on
4
+ PyPI, print a one-time banner to stderr telling the user how to upgrade.
5
+ The PyPI fetch happens in a background daemon thread, so a stale cache is
6
+ refreshed for *next* time. The current run never blocks on the network.
7
+
8
+ ``dxs update`` performs the upgrade itself. The mechanics differ by
9
+ platform because of how each OS treats files belonging to a running
10
+ process:
11
+
12
+ - POSIX (macOS/Linux): ``os.execvp`` replaces this process with the
13
+ upgrader (uv/pipx/pip). Replacing files out from under a dead process
14
+ is a non-event on POSIX, and the upgrader's output lands in the same
15
+ terminal.
16
+ - Windows: the ``dxs.exe`` launcher shim holds an exclusive image lock
17
+ until the *whole* process tree exits (the shim spawns python and
18
+ waits), and loaded ``.pyd`` files are locked too — so no in-process
19
+ approach can work. Instead we spawn a detached ``cmd`` helper that
20
+ waits for both PIDs to exit (gated on image name, bounded), runs the
21
+ upgrade with output captured to ``~/.datex/update.log``, and a pending
22
+ marker lets the *next* dxs run report the outcome.
23
+
24
+ Environment / flag controls:
25
+ - ``DXS_NO_UPDATE_CHECK=1`` disables both the check and the banner.
26
+ - ``--no-update-check`` (CLI flag) does the same per-invocation.
27
+ - ``DXS_UPDATE_CHECK_DEBUG=1`` surfaces sender errors (development only).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import os
34
+ import re
35
+ import subprocess
36
+ import sys
37
+ import threading
38
+ import time
39
+ from pathlib import Path
40
+ from typing import Any
41
+
42
+ import click
43
+
44
+ from dxs import __version__
45
+ from dxs.utils.paths import datex_home_path, get_datex_home
46
+
47
+ PYPI_URL = "https://pypi.org/pypi/datex-studio-cli/json"
48
+ PACKAGE_NAME = "datex-studio-cli"
49
+ CACHE_TTL_SECONDS = 24 * 60 * 60
50
+ NOTIFY_DEDUPE_SECONDS = 60 * 60 # at most one banner per hour per machine
51
+ HTTP_TIMEOUT_SECONDS = 2.0
52
+
53
+ UPDATE_LOG_FILE = "update.log"
54
+ UPDATE_HELPER_FILE = "update_helper.cmd"
55
+ UPDATE_PENDING_FILE = "update_pending.json"
56
+ # After this long, a pending background upgrade that hasn't landed is
57
+ # reported as failed (with the log path) instead of silently forgotten.
58
+ UPDATE_PENDING_STALE_SECONDS = 15 * 60
59
+
60
+ _HELPER_MAX_TRIES = 300 # ~5 min of 1s polls per waited-on PID
61
+
62
+ _CREATE_NO_WINDOW = 0x08000000
63
+ _CREATE_NEW_PROCESS_GROUP = 0x00000200
64
+ _CREATE_BREAKAWAY_FROM_JOB = 0x01000000
65
+
66
+ _CI_ENV_VARS = ("CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "BUILDKITE", "TF_BUILD")
67
+
68
+
69
+ def _cache_path() -> Path:
70
+ return get_datex_home() / "update_check.json"
71
+
72
+
73
+ def _debug(msg: str) -> None:
74
+ if os.environ.get("DXS_UPDATE_CHECK_DEBUG", "").lower() in ("1", "true", "yes"):
75
+ click.echo(f"[update-check] {msg}", err=True)
76
+
77
+
78
+ def is_disabled() -> bool:
79
+ """Return True if the update check has been disabled via env var."""
80
+ return os.environ.get("DXS_NO_UPDATE_CHECK", "").lower() in ("1", "true", "yes")
81
+
82
+
83
+ def _is_ci() -> bool:
84
+ return any(os.environ.get(v) for v in _CI_ENV_VARS)
85
+
86
+
87
+ def _read_cache() -> dict[str, Any] | None:
88
+ path = _cache_path()
89
+ if not path.exists():
90
+ return None
91
+ try:
92
+ data = json.loads(path.read_text())
93
+ if isinstance(data, dict):
94
+ return data
95
+ except (json.JSONDecodeError, OSError) as e:
96
+ _debug(f"cache read failed: {e}")
97
+ return None
98
+
99
+
100
+ def _write_cache(data: dict[str, Any]) -> None:
101
+ try:
102
+ _cache_path().write_text(json.dumps(data))
103
+ except OSError as e:
104
+ _debug(f"cache write failed: {e}")
105
+
106
+
107
+ def _fetch_latest_version() -> str | None:
108
+ """Fetch the latest version from PyPI. Returns None on any failure."""
109
+ try:
110
+ import httpx
111
+
112
+ response = httpx.get(PYPI_URL, timeout=HTTP_TIMEOUT_SECONDS)
113
+ response.raise_for_status()
114
+ latest = response.json()["info"]["version"]
115
+ return str(latest) if latest else None
116
+ except Exception as e: # noqa: BLE001 - never let telemetry break the CLI
117
+ _debug(f"fetch failed: {e}")
118
+ return None
119
+
120
+
121
+ def _refresh_cache_worker() -> None:
122
+ latest = _fetch_latest_version()
123
+ if not latest:
124
+ return
125
+ cache = _read_cache() or {}
126
+ cache["latest"] = latest
127
+ cache["checked_at"] = time.time()
128
+ _write_cache(cache)
129
+
130
+
131
+ def _maybe_refresh_async(cache: dict[str, Any] | None) -> None:
132
+ """Spawn a daemon thread to refresh the cache if it's missing or stale."""
133
+ needs_refresh = (
134
+ cache is None or (time.time() - float(cache.get("checked_at", 0))) > CACHE_TTL_SECONDS
135
+ )
136
+ if not needs_refresh:
137
+ return
138
+ thread = threading.Thread(target=_refresh_cache_worker, daemon=True, name="dxs-update-check")
139
+ thread.start()
140
+
141
+
142
+ _VERSION_RE = re.compile(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?")
143
+
144
+
145
+ def _parse_version(v: str) -> tuple[int, int, int]:
146
+ """Parse an X.Y.Z version into a tuple. Pre-release suffixes are ignored.
147
+
148
+ Returns (0, 0, 0) on unparseable input — treated as oldest possible.
149
+ """
150
+ m = _VERSION_RE.match(v.strip())
151
+ if not m:
152
+ return (0, 0, 0)
153
+ return (int(m.group(1) or 0), int(m.group(2) or 0), int(m.group(3) or 0))
154
+
155
+
156
+ def is_newer(candidate: str, current: str) -> bool:
157
+ """Return True if `candidate` is a newer version than `current`."""
158
+ return _parse_version(candidate) > _parse_version(current)
159
+
160
+
161
+ def detect_upgrade_argv() -> list[str]:
162
+ """Best-effort detection of how dxs was installed, as an argv list."""
163
+ exe = sys.executable
164
+ # uv tool installs land under ~/.local/share/uv/tools/<pkg>/...
165
+ if "/uv/tools/" in exe or "\\uv\\tools\\" in exe:
166
+ return ["uv", "tool", "upgrade", PACKAGE_NAME]
167
+ # pipx installs land under ~/.local/pipx/venvs/<pkg>/...
168
+ if "/pipx/" in exe or "\\pipx\\" in exe:
169
+ return ["pipx", "upgrade", PACKAGE_NAME]
170
+ # Fallback: pip in whatever interpreter the user is on
171
+ return [exe, "-m", "pip", "install", "--upgrade", PACKAGE_NAME]
172
+
173
+
174
+ def detect_upgrade_command() -> str:
175
+ """The upgrade command as a display string (for banners and --check)."""
176
+ return " ".join(detect_upgrade_argv())
177
+
178
+
179
+ def is_windows() -> bool:
180
+ """Seam for the platform split — patchable in tests on any host."""
181
+ return sys.platform == "win32"
182
+
183
+
184
+ def is_dev_install() -> bool:
185
+ """True when dxs runs from a source checkout or local/editable install.
186
+
187
+ Such installs must not be upgraded from PyPI — ``git pull && uv sync``
188
+ is the correct path. Detection reads the distribution's
189
+ ``direct_url.json``: present only for local-path installs (PyPI installs
190
+ don't have one). No distribution record at all also counts as dev.
191
+ """
192
+ try:
193
+ from importlib.metadata import distribution
194
+
195
+ dist = distribution(PACKAGE_NAME)
196
+ except Exception: # noqa: BLE001 - not installed as a distribution
197
+ return True
198
+ try:
199
+ text = dist.read_text("direct_url.json")
200
+ except Exception: # noqa: BLE001
201
+ return False
202
+ if not text:
203
+ return False
204
+ try:
205
+ info = json.loads(text)
206
+ except json.JSONDecodeError:
207
+ return False
208
+ dir_info = info.get("dir_info")
209
+ if isinstance(dir_info, dict) and dir_info.get("editable"):
210
+ return True
211
+ url = info.get("url", "")
212
+ return isinstance(url, str) and url.startswith("file://")
213
+
214
+
215
+ def perform_upgrade_posix(argv: list[str]) -> None:
216
+ """Replace this process with the upgrader. Does not return on success.
217
+
218
+ The exec frees every file handle before uv/pipx/pip starts rewriting
219
+ the venv, and the upgrader inherits the terminal for its output.
220
+ Raises OSError if the exec itself fails to start.
221
+ """
222
+ sys.stdout.flush()
223
+ sys.stderr.flush()
224
+ os.execvp(argv[0], argv) # noqa: S606 - deliberate process replacement
225
+
226
+
227
+ def _windows_helper_script(argv: list[str], pid: int, ppid: int, log_path: Path) -> str:
228
+ """Generate the detached cmd helper that performs a Windows upgrade.
229
+
230
+ The helper waits for two PIDs: this python process and its parent —
231
+ the ``dxs.exe`` trampoline that holds the image-file lock. Each wait is
232
+ gated on the image name (``python``/``dxs``) so a PID that belongs to
233
+ something else — dxs launched from a shell, or a recycled PID — is
234
+ never awaited, and bounded so a wedged process can't pin the helper
235
+ forever (the upgrade then runs anyway and the log records the real
236
+ error). ``ping -n 2`` is the sleep: ``timeout`` requires an
237
+ interactive console and fails in a detached process.
238
+ """
239
+ cmdline = subprocess.list2cmdline(argv)
240
+ log = str(log_path)
241
+ lines = [
242
+ "@echo off",
243
+ "setlocal",
244
+ "set TRIES=0",
245
+ f'echo [dxs update] waiting for dxs (pids {pid}, {ppid}) to exit... > "{log}"',
246
+ ":wait_self",
247
+ "set /a TRIES+=1",
248
+ f"if %TRIES% GTR {_HELPER_MAX_TRIES} goto run",
249
+ f'tasklist /FI "PID eq {pid}" /FO CSV /NH 2>nul | findstr /i "python" >nul',
250
+ "if not errorlevel 1 (",
251
+ "ping -n 2 127.0.0.1 >nul",
252
+ "goto wait_self",
253
+ ")",
254
+ ":wait_parent",
255
+ "set /a TRIES+=1",
256
+ f"if %TRIES% GTR {_HELPER_MAX_TRIES} goto run",
257
+ f'tasklist /FI "PID eq {ppid}" /FO CSV /NH 2>nul | findstr /i "dxs" >nul',
258
+ "if not errorlevel 1 (",
259
+ "ping -n 2 127.0.0.1 >nul",
260
+ "goto wait_parent",
261
+ ")",
262
+ ":run",
263
+ f'echo [dxs update] running: {cmdline} >> "{log}"',
264
+ f'{cmdline} >> "{log}" 2>&1',
265
+ f'echo [dxs update] upgrader exit code: %errorlevel% >> "{log}"',
266
+ "endlocal",
267
+ "",
268
+ ]
269
+ return "\r\n".join(lines)
270
+
271
+
272
+ def _spawn_detached_helper(helper_path: Path) -> None:
273
+ """Launch the helper fully detached so it survives this process's exit.
274
+
275
+ CREATE_BREAKAWAY_FROM_JOB is attempted first in case the launcher put
276
+ us in a kill-on-close job object; if the job refuses breakaway the
277
+ spawn is retried without it. CREATE_NO_WINDOW keeps the helper's
278
+ console invisible.
279
+ """
280
+ argv = ["cmd.exe", "/d", "/c", str(helper_path)]
281
+ common: dict[str, Any] = {
282
+ "stdin": subprocess.DEVNULL,
283
+ "stdout": subprocess.DEVNULL,
284
+ "stderr": subprocess.DEVNULL,
285
+ "close_fds": True,
286
+ "cwd": str(helper_path.parent),
287
+ }
288
+ flags = _CREATE_NO_WINDOW | _CREATE_NEW_PROCESS_GROUP
289
+ try:
290
+ subprocess.Popen(argv, creationflags=flags | _CREATE_BREAKAWAY_FROM_JOB, **common)
291
+ except OSError:
292
+ subprocess.Popen(argv, creationflags=flags, **common)
293
+
294
+
295
+ def perform_upgrade_windows(argv: list[str], latest: str) -> Path:
296
+ """Write the helper + pending marker and spawn the detached upgrade.
297
+
298
+ Returns the log path the helper will write to. Raises OSError if the
299
+ helper cannot be written or spawned (the marker is cleared so the
300
+ next run doesn't report a phantom failure).
301
+ """
302
+ home = get_datex_home()
303
+ log_path = home / UPDATE_LOG_FILE
304
+ helper_path = home / UPDATE_HELPER_FILE
305
+ script = _windows_helper_script(argv, pid=os.getpid(), ppid=os.getppid(), log_path=log_path)
306
+ helper_path.write_text(script, newline="")
307
+ _write_pending(
308
+ {
309
+ "from_version": __version__,
310
+ "to_version": latest,
311
+ "log": str(log_path),
312
+ "started_at": time.time(),
313
+ }
314
+ )
315
+ try:
316
+ _spawn_detached_helper(helper_path)
317
+ except OSError:
318
+ _clear_pending()
319
+ raise
320
+ return log_path
321
+
322
+
323
+ def count_windows_dxs_processes() -> int | None:
324
+ """Count running ``dxs.exe`` launchers (Windows only). None if unknown.
325
+
326
+ More than one means another dxs (``dxs studio``, another terminal) holds
327
+ venv locks that will fail the background upgrade until it exits.
328
+ """
329
+ try:
330
+ out = subprocess.run(
331
+ ["tasklist", "/FI", "IMAGENAME eq dxs.exe", "/FO", "CSV", "/NH"],
332
+ capture_output=True,
333
+ text=True,
334
+ timeout=10,
335
+ ).stdout
336
+ return sum(1 for line in out.splitlines() if line.lower().startswith('"dxs.exe"'))
337
+ except Exception: # noqa: BLE001 - preflight is best-effort
338
+ return None
339
+
340
+
341
+ def _pending_path() -> Path:
342
+ # Read on every invocation via the post-command hook — must not mkdir,
343
+ # or `dxs reset` would see ~/.datex/ resurrected immediately.
344
+ return datex_home_path() / UPDATE_PENDING_FILE
345
+
346
+
347
+ def _write_pending(data: dict[str, Any]) -> None:
348
+ (get_datex_home() / UPDATE_PENDING_FILE).write_text(json.dumps(data))
349
+
350
+
351
+ def _clear_pending() -> None:
352
+ try:
353
+ _pending_path().unlink()
354
+ except OSError:
355
+ pass
356
+
357
+
358
+ def maybe_show_update_result() -> None:
359
+ """One-shot outcome notice for a background (Windows) upgrade.
360
+
361
+ The detached helper can't print to the original console, so the marker
362
+ written at spawn time is judged here on the next run: current version
363
+ reached the target → success; still on the old version past the stale
364
+ window → failure, pointing at the log. In between, stay quiet and keep
365
+ the marker.
366
+ """
367
+ path = _pending_path()
368
+ if not path.exists():
369
+ return
370
+ try:
371
+ data = json.loads(path.read_text())
372
+ to_version = data["to_version"]
373
+ started_at = float(data.get("started_at", 0))
374
+ log = data.get("log", "")
375
+ if not isinstance(to_version, str):
376
+ raise TypeError("to_version must be a string")
377
+ except Exception: # noqa: BLE001 - corrupt marker: clear and move on
378
+ _clear_pending()
379
+ return
380
+
381
+ if not is_newer(to_version, __version__):
382
+ click.echo(f"dxs upgrade complete — now running {__version__}.", err=True)
383
+ _clear_pending()
384
+ elif time.time() - started_at > UPDATE_PENDING_STALE_SECONDS:
385
+ click.echo(
386
+ f"A background upgrade to dxs {to_version} was started but you are "
387
+ f"still on {__version__} — it appears to have failed.",
388
+ err=True,
389
+ )
390
+ if log:
391
+ click.echo(f"See the log: {log}", err=True)
392
+ _clear_pending()
393
+
394
+
395
+ def _stderr_is_tty() -> bool:
396
+ """True when stderr is attached to a TTY.
397
+
398
+ Only stderr matters — the banner is written there. Piping stdout (e.g.
399
+ ``dxs odata ... | jq``) leaves stderr untouched, so the user still sees
400
+ the banner in their terminal. Redirecting stderr (``2>log``) suppresses
401
+ it, as intended.
402
+ """
403
+ return sys.stderr.isatty()
404
+
405
+
406
+ def _print_banner(latest: str, upgrade_cmd: str) -> None:
407
+ """Print the update banner to stderr."""
408
+ click.echo("", err=True)
409
+ click.echo(
410
+ f" A new release of dxs is available: {__version__} → {latest}",
411
+ err=True,
412
+ )
413
+ click.echo(f" To upgrade, run: {upgrade_cmd}", err=True)
414
+ click.echo(f" https://pypi.org/project/{PACKAGE_NAME}/{latest}/", err=True)
415
+
416
+
417
+ def maybe_show_update_notice(skip: bool = False) -> None:
418
+ """If the cache shows a newer version, print a one-time banner to stderr.
419
+
420
+ Always returns quickly — the PyPI fetch happens in a background thread
421
+ that only writes to the cache. The first run on a stale cache prints
422
+ nothing; the next run sees the refreshed cache and prints the banner.
423
+ Dedupe keeps it to one banner per ``NOTIFY_DEDUPE_SECONDS`` window so
424
+ fast-fire commands don't spam.
425
+
426
+ Args:
427
+ skip: If True, skip both the check and any cache refresh. Used to
428
+ honor ``--no-update-check``.
429
+ """
430
+ if skip or is_disabled() or _is_ci():
431
+ return
432
+ if not _stderr_is_tty():
433
+ return
434
+
435
+ cache = _read_cache()
436
+ _maybe_refresh_async(cache)
437
+
438
+ if not cache:
439
+ return
440
+
441
+ latest = cache.get("latest")
442
+ if not isinstance(latest, str) or not is_newer(latest, __version__):
443
+ return
444
+
445
+ last_notified = float(cache.get("last_notified_at", 0))
446
+ if time.time() - last_notified < NOTIFY_DEDUPE_SECONDS:
447
+ return
448
+
449
+ _print_banner(latest, detect_upgrade_command())
450
+
451
+ cache["last_notified_at"] = time.time()
452
+ _write_cache(cache)