datex-studio-cli 0.5.1__tar.gz → 0.5.2__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 (235) hide show
  1. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/ci.yml +29 -0
  2. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/PKG-INFO +1 -1
  3. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/pyproject.toml +3 -3
  4. datex_studio_cli-0.5.2/scripts/check_wheel_completeness.py +58 -0
  5. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/__init__.py +1 -1
  6. datex_studio_cli-0.5.2/src/dxs/core/quartermaster/__init__.py +5 -0
  7. datex_studio_cli-0.5.2/src/dxs/core/quartermaster/client.py +178 -0
  8. datex_studio_cli-0.5.2/src/dxs/core/skills/__init__.py +1 -0
  9. datex_studio_cli-0.5.2/src/dxs/core/skills/frontmatter.py +30 -0
  10. datex_studio_cli-0.5.2/src/dxs/core/skills/installer.py +148 -0
  11. datex_studio_cli-0.5.2/src/dxs/core/skills/layout.py +57 -0
  12. datex_studio_cli-0.5.2/src/dxs/core/skills/lock.py +98 -0
  13. datex_studio_cli-0.5.2/src/dxs/core/skills/resolver.py +48 -0
  14. datex_studio_cli-0.5.2/src/dxs/core/skills/staleness.py +55 -0
  15. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/404/index.html +1 -1
  16. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/404.html +1 -1
  17. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/capture/index.html +1 -1
  18. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/capture/index.txt +1 -1
  19. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/index.html +1 -1
  20. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/index.txt +1 -1
  21. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/index.html +1 -1
  22. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/index.txt +1 -1
  23. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.dockerignore +0 -0
  24. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/actionlint.yaml +0 -0
  25. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/build-image.yml +0 -0
  26. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/promote-prod.yml +0 -0
  27. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/release.yml +0 -0
  28. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.gitignore +0 -0
  29. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/CONTEXT.md +0 -0
  30. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/README.md +0 -0
  31. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/.env.example +0 -0
  32. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/Dockerfile +0 -0
  33. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/README.md +0 -0
  34. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/apply-local-cache.js +0 -0
  35. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/collector-config.yaml +0 -0
  36. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/docker-compose.yml +0 -0
  37. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/local-test-settings.json +0 -0
  38. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/remote-settings.json +0 -0
  39. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/report-creator.skill +0 -0
  40. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/analyze_corpus.py +0 -0
  41. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/datadog_errors.py +0 -0
  42. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/generate_rdlx_schema.py +0 -0
  43. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/wavelength_query.py +0 -0
  44. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/__main__.py +0 -0
  45. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/cli.py +0 -0
  46. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/__init__.py +0 -0
  47. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/api.py +0 -0
  48. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/auth.py +0 -0
  49. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/branch.py +0 -0
  50. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/branch_commit_guard.py +0 -0
  51. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/cascade.py +0 -0
  52. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/config.py +0 -0
  53. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/configuration.py +0 -0
  54. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/crm.py +0 -0
  55. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/crm_delivery.py +0 -0
  56. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/crm_time.py +0 -0
  57. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/datasource.py +0 -0
  58. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/devops.py +0 -0
  59. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/document.py +0 -0
  60. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/endpoint.py +0 -0
  61. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/env.py +0 -0
  62. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/explore.py +0 -0
  63. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/function.py +0 -0
  64. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/init.py +0 -0
  65. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/marketplace.py +0 -0
  66. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/__init__.py +0 -0
  67. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/_common.py +0 -0
  68. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/api.py +0 -0
  69. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/create.py +0 -0
  70. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/data.py +0 -0
  71. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/pack.py +0 -0
  72. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/preview.py +0 -0
  73. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/odata.py +0 -0
  74. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/organization.py +0 -0
  75. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/proxy.py +0 -0
  76. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/reference.py +0 -0
  77. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/release_notes.py +0 -0
  78. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/repo.py +0 -0
  79. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/__init__.py +0 -0
  80. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/add_cmds.py +0 -0
  81. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/api.py +0 -0
  82. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/batch.py +0 -0
  83. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/create.py +0 -0
  84. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/data.py +0 -0
  85. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/dataset.py +0 -0
  86. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/edit.py +0 -0
  87. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/folder.py +0 -0
  88. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/preview.py +0 -0
  89. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/schema_cmds.py +0 -0
  90. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/reset.py +0 -0
  91. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/schema.py +0 -0
  92. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/servicepack.py +0 -0
  93. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/skills.py +0 -0
  94. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/source.py +0 -0
  95. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/studio.py +0 -0
  96. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/telemetry.py +0 -0
  97. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/user.py +0 -0
  98. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/validation_report.py +0 -0
  99. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/context.py +0 -0
  100. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/__init__.py +0 -0
  101. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/agent_browser.py +0 -0
  102. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/__init__.py +0 -0
  103. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/app_config.py +0 -0
  104. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/client.py +0 -0
  105. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/endpoints.py +0 -0
  106. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/metadata_models.py +0 -0
  107. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/models.py +0 -0
  108. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/reference_graph.py +0 -0
  109. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/replacements.py +0 -0
  110. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/__init__.py +0 -0
  111. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/datex_tenant.py +0 -0
  112. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/decorators.py +0 -0
  113. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/msal_client.py +0 -0
  114. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/tenants.py +0 -0
  115. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/token_cache.py +0 -0
  116. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/cache.py +0 -0
  117. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/__init__.py +0 -0
  118. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/flow_generator.py +0 -0
  119. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/flow_models.py +0 -0
  120. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/flow_shape.py +0 -0
  121. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/generator.py +0 -0
  122. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/models.py +0 -0
  123. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/parsers.py +0 -0
  124. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/resolver.py +0 -0
  125. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/type_def_parser.py +0 -0
  126. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/validator.py +0 -0
  127. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/__init__.py +0 -0
  128. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/flow_models.py +0 -0
  129. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/models.py +0 -0
  130. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/parsers.py +0 -0
  131. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/__init__.py +0 -0
  132. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/client.py +0 -0
  133. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/helpers.py +0 -0
  134. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/models.py +0 -0
  135. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/__init__.py +0 -0
  136. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/client.py +0 -0
  137. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/html.py +0 -0
  138. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  139. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/pagination.py +0 -0
  140. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/resolvers.py +0 -0
  141. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/time_entry.py +0 -0
  142. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/__init__.py +0 -0
  143. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/client.py +0 -0
  144. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/edmx_parser.py +0 -0
  145. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/metadata.py +0 -0
  146. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/__init__.py +0 -0
  147. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/generator.py +0 -0
  148. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/models.py +0 -0
  149. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/validator.py +0 -0
  150. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/graph.py +0 -0
  151. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/onboarding.py +0 -0
  152. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/__init__.py +0 -0
  153. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/csv_fmt.py +0 -0
  154. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/formatter.py +0 -0
  155. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/json_fmt.py +0 -0
  156. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/redaction.py +0 -0
  157. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/yaml_fmt.py +0 -0
  158. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/release_notes.py +0 -0
  159. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/responses.py +0 -0
  160. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/__init__.py +0 -0
  161. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/config.py +0 -0
  162. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/identity.py +0 -0
  163. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/recorder.py +0 -0
  164. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/scrubber.py +0 -0
  165. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/sender.py +0 -0
  166. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/models/__init__.py +0 -0
  167. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/__init__.py +0 -0
  168. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/folder.py +0 -0
  169. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/manifest.py +0 -0
  170. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/mocks.py +0 -0
  171. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/preview.py +0 -0
  172. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/scaffold.py +0 -0
  173. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/serve_lock.py +0 -0
  174. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/py.typed +0 -0
  175. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/__init__.py +0 -0
  176. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/datasource_binding.py +0 -0
  177. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/engine.py +0 -0
  178. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/field_parser.py +0 -0
  179. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/folder.py +0 -0
  180. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/manifest.py +0 -0
  181. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/models.py +0 -0
  182. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/preview.py +0 -0
  183. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/preview_arjs.py +0 -0
  184. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/schema.py +0 -0
  185. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/svg_renderer.py +0 -0
  186. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/templates/__init__.py +0 -0
  187. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  188. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  189. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/validate_arjs.py +0 -0
  190. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/validator.py +0 -0
  191. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/wrapper.py +0 -0
  192. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/__init__.py +0 -0
  193. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/click_options.py +0 -0
  194. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/clipboard.py +0 -0
  195. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/config.py +0 -0
  196. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/errors.py +0 -0
  197. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/filtering.py +0 -0
  198. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/image.py +0 -0
  199. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/paths.py +0 -0
  200. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/resolvers.py +0 -0
  201. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/responses.py +0 -0
  202. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/restricted.py +0 -0
  203. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/sorting.py +0 -0
  204. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/update_check.py +0 -0
  205. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/versioning.py +0 -0
  206. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/__init__.py +0 -0
  207. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/app.py +0 -0
  208. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/design.py +0 -0
  209. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/proxy_app.py +0 -0
  210. {datex_studio_cli-0.5.1/src/dxs/web/static/_next/static/W6-87g3VtSLhTM2G0U3r1 → datex_studio_cli-0.5.2/src/dxs/web/static/_next/static/5ccHgxTeTrw5DjrFowRBg}/_buildManifest.js +0 -0
  211. {datex_studio_cli-0.5.1/src/dxs/web/static/_next/static/W6-87g3VtSLhTM2G0U3r1 → datex_studio_cli-0.5.2/src/dxs/web/static/_next/static/5ccHgxTeTrw5DjrFowRBg}/_ssgManifest.js +0 -0
  212. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  213. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  214. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  215. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/538-b06224e65f71c9a1.js +0 -0
  216. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  217. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  218. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  219. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  220. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-e3c660196c244183.js +0 -0
  221. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-c083dc9f01f32a04.js +0 -0
  222. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/app/design/page-689cd5b9e1d9a74e.js +0 -0
  223. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/app/layout-cec86f90e385371d.js +0 -0
  224. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/app/page-f0380b0f29983264.js +0 -0
  225. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  226. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  227. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/main-app-694cf3568b6c9fa9.js +0 -0
  228. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  229. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  230. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  231. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  232. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  233. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/render-cli.js +0 -0
  234. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/report-preview.html +0 -0
  235. {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/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.2
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.2"
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.2"
4
4
  __app_name__ = "dxs"
@@ -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,178 @@
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(self, path: str, params: dict[str, Any] | None, accept: str) -> httpx.Response:
113
+ headers = {"Authorization": f"Bearer {get_access_token()}", "Accept": accept}
114
+ try:
115
+ response = self._http().get(path, params=params, headers=headers)
116
+ except httpx.HTTPError as exc:
117
+ raise QuartermasterError(
118
+ f"Could not reach Quartermaster at {self.base_url}: {exc}",
119
+ code="DXS-QM-004",
120
+ suggestions=[
121
+ "Check your network connection",
122
+ "If you point at a staging or local instance, verify "
123
+ "DXS_QUARTERMASTER_BASE_URL",
124
+ ],
125
+ ) from exc
126
+ self._raise_for_status(path, response)
127
+ return response
128
+
129
+ def _raise_for_status(self, path: str, response: httpx.Response) -> None:
130
+ if response.is_success:
131
+ return
132
+ status = response.status_code
133
+ if status == 401:
134
+ raise AuthenticationError(
135
+ message="Quartermaster rejected the access token",
136
+ code="DXS-AUTH-001",
137
+ suggestions=["Run 'dxs auth login' to refresh your session"],
138
+ )
139
+ error = _structured_error(response)
140
+ if error is not None:
141
+ message = str(error.get("message") or f"Quartermaster returned {status}")
142
+ extra = {k: v for k, v in error.items() if k not in ("code", "message")}
143
+ raise QuartermasterError(
144
+ message,
145
+ service_code=str(error.get("code")) if error.get("code") else None,
146
+ status_code=status,
147
+ details=extra or None,
148
+ )
149
+ if status == 404:
150
+ route = _family_and_segment(path)
151
+ raise QuartermasterError(
152
+ f"Quartermaster at {self.base_url} doesn't serve {route} — "
153
+ "the service may be older than your CLI",
154
+ code="DXS-QM-002",
155
+ status_code=status,
156
+ suggestions=[
157
+ "Check DXS_QUARTERMASTER_BASE_URL points at a current Quartermaster",
158
+ ],
159
+ )
160
+ raise QuartermasterError(
161
+ f"Quartermaster returned HTTP {status} for {path}",
162
+ status_code=status,
163
+ )
164
+
165
+ def get_json(self, path: str, params: dict[str, Any] | None = None) -> Any:
166
+ response = self._request(path, params, "application/json")
167
+ try:
168
+ return response.json()
169
+ except ValueError as exc:
170
+ raise QuartermasterError(
171
+ f"Quartermaster returned a non-JSON body for {path}", status_code=200
172
+ ) from exc
173
+
174
+ def get_bytes(
175
+ self, path: str, params: dict[str, Any] | None = None
176
+ ) -> tuple[bytes, dict[str, str]]:
177
+ response = self._request(path, params, "application/gzip, application/json")
178
+ return response.content, {k.lower(): v for k, v in response.headers.items()}
@@ -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 []
@@ -0,0 +1,148 @@
1
+ """Write a skill bundle into a layout and link it into agent directories.
2
+
3
+ Safety: the tarball is extracted into a scratch directory first — an entry
4
+ that escapes it (``..``, absolute path, link) aborts before anything under
5
+ the layout is touched. Each skill's canonical directory is then replaced
6
+ wholesale, so files removed upstream do not linger.
7
+
8
+ Agent directories: an existing symlink is repointed (an ``npx skills``
9
+ install adopted in place); a real directory is left alone and reported as a
10
+ conflict unless ``force``. Where symlinks are not permitted (Windows without
11
+ developer mode) the canonical copy is copied instead, and the lock records
12
+ ``linkMode: copy`` so ``remove`` knows to delete rather than unlink.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import io
18
+ import os
19
+ import shutil
20
+ import tarfile
21
+ import tempfile
22
+ from collections.abc import Iterable
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ from dxs.utils.errors import DxsError
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class InstallResult:
31
+ name: str
32
+ link_mode: str
33
+ conflicts: tuple[str, ...]
34
+
35
+
36
+ def _unsafe(message: str) -> DxsError:
37
+ return DxsError(
38
+ code="DXS-SKILLS-004",
39
+ message=f"Refusing to install skills bundle: {message}",
40
+ suggestions=["Report this — the bundle Quartermaster served was malformed"],
41
+ )
42
+
43
+
44
+ def _extract(bundle: bytes, scratch: Path) -> list[str]:
45
+ scratch_resolved = scratch.resolve()
46
+ order: dict[str, None] = {}
47
+ try:
48
+ with tarfile.open(fileobj=io.BytesIO(bundle), mode="r:gz") as tar:
49
+ for member in tar.getmembers():
50
+ order.setdefault(member.name.split("/", 1)[0], None)
51
+ if member.issym() or member.islnk():
52
+ raise _unsafe(f"archive contains a link: {member.name}")
53
+ if os.path.isabs(member.name):
54
+ raise _unsafe(f"archive entry is absolute: {member.name}")
55
+ target = (scratch_resolved / member.name).resolve()
56
+ if target != scratch_resolved and scratch_resolved not in target.parents:
57
+ raise _unsafe(f"archive entry escapes the bundle: {member.name}")
58
+ if not member.isdir() and not member.isfile():
59
+ raise _unsafe(f"archive entry has an unsupported type: {member.name}")
60
+ tar.extractall(scratch_resolved) # noqa: S202 - members were checked above
61
+ except tarfile.TarError as exc:
62
+ raise _unsafe(f"corrupt archive ({exc})") from exc
63
+ # Order of first appearance == the order the service packed them == request order.
64
+ return [name for name in order if (scratch_resolved / name).is_dir()]
65
+
66
+
67
+ def _replace_dir(src: Path, dest: Path) -> None:
68
+ dest.parent.mkdir(parents=True, exist_ok=True)
69
+ if dest.is_symlink() or dest.is_file():
70
+ dest.unlink()
71
+ elif dest.is_dir():
72
+ shutil.rmtree(dest)
73
+ shutil.move(str(src), str(dest))
74
+
75
+
76
+ def _link(canonical: Path, target: Path, *, force: bool) -> tuple[str, bool]:
77
+ """Point ``target`` at ``canonical``. Returns ``(link_mode, conflicted)``."""
78
+ target.parent.mkdir(parents=True, exist_ok=True)
79
+ if target.is_symlink():
80
+ target.unlink()
81
+ elif target.exists():
82
+ if not force:
83
+ return "symlink", True
84
+ shutil.rmtree(target) if target.is_dir() else target.unlink()
85
+ try:
86
+ os.symlink(canonical, target, target_is_directory=True)
87
+ return "symlink", False
88
+ except (OSError, NotImplementedError):
89
+ shutil.copytree(canonical, target)
90
+ return "copy", False
91
+
92
+
93
+ def install_bundle(
94
+ bundle: bytes,
95
+ layout: object,
96
+ agents: Iterable[str],
97
+ *,
98
+ force: bool = False,
99
+ ) -> list[InstallResult]:
100
+ from dxs.core.skills.layout import Layout
101
+
102
+ assert isinstance(layout, Layout)
103
+ agent_list = list(agents)
104
+ results: list[InstallResult] = []
105
+ with tempfile.TemporaryDirectory(prefix="dxs-skills-") as tmp:
106
+ scratch = Path(tmp)
107
+ names = _extract(bundle, scratch)
108
+ for name in names:
109
+ canonical = layout.canonical / name
110
+ _replace_dir(scratch / name, canonical)
111
+ link_mode = "symlink"
112
+ conflicts: list[str] = []
113
+ for agent in agent_list:
114
+ mode, conflicted = _link(canonical, layout.agent_dirs[agent] / name, force=force)
115
+ if conflicted:
116
+ conflicts.append(agent)
117
+ elif mode == "copy":
118
+ link_mode = "copy"
119
+ results.append(
120
+ InstallResult(name=name, link_mode=link_mode, conflicts=tuple(conflicts))
121
+ )
122
+ return results
123
+
124
+
125
+ def remove_skill(name: str, layout: object, agents: Iterable[str], link_mode: str) -> bool:
126
+ """Delete a skill's canonical copy and the agent entries that belong to it."""
127
+ from dxs.core.skills.layout import Layout
128
+
129
+ assert isinstance(layout, Layout)
130
+ canonical = layout.canonical / name
131
+ removed = False
132
+ for agent in agents:
133
+ target = layout.agent_dirs[agent] / name
134
+ if target.is_symlink():
135
+ try:
136
+ points_at_us = target.resolve() == canonical.resolve()
137
+ except OSError:
138
+ points_at_us = False
139
+ if points_at_us or not canonical.exists():
140
+ target.unlink()
141
+ removed = True
142
+ elif link_mode == "copy" and target.is_dir():
143
+ shutil.rmtree(target)
144
+ removed = True
145
+ if canonical.is_dir():
146
+ shutil.rmtree(canonical)
147
+ removed = True
148
+ return removed
@@ -0,0 +1,57 @@
1
+ """Where skills live on disk — parity with ``npx skills`` so agents need no change.
2
+
3
+ Canonical copies sit in ``<root>/.agents/skills/<name>``; each agent directory
4
+ (``.claude/skills``, ``.codex/skills``) gets a symlink to the canonical copy.
5
+ User scope roots at ``~``; project scope at the current working directory.
6
+ Our install record is a ``skills-lock.json`` in the ``npx skills`` shape
7
+ (``~/.datex/skills-lock.json`` for user scope, ``<project>/skills-lock.json``
8
+ for project scope) — see ``lock.py``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Literal
16
+
17
+ Scope = Literal["user", "project"]
18
+
19
+ #: Phase 1 targets (issue #95). Phase 2 grows this to the vercel-labs/skills list.
20
+ AGENTS: dict[str, str] = {"claude": ".claude/skills", "codex": ".codex/skills"}
21
+ CANONICAL_DIR = ".agents/skills"
22
+ LOCK_FILE = "skills-lock.json"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Layout:
27
+ scope: Scope
28
+ root: Path
29
+ canonical: Path
30
+ agent_dirs: dict[str, Path]
31
+ lock_path: Path
32
+
33
+
34
+ def user_layout() -> Layout:
35
+ home = Path.home()
36
+ return Layout(
37
+ scope="user",
38
+ root=home,
39
+ canonical=home / CANONICAL_DIR,
40
+ agent_dirs={agent: home / rel for agent, rel in AGENTS.items()},
41
+ lock_path=home / ".datex" / LOCK_FILE,
42
+ )
43
+
44
+
45
+ def project_layout(root: Path) -> Layout:
46
+ root = root.resolve()
47
+ return Layout(
48
+ scope="project",
49
+ root=root,
50
+ canonical=root / CANONICAL_DIR,
51
+ agent_dirs={agent: root / rel for agent, rel in AGENTS.items()},
52
+ lock_path=root / LOCK_FILE,
53
+ )
54
+
55
+
56
+ def layout_for(project: bool, cwd: Path | None = None) -> Layout:
57
+ return project_layout(cwd or Path.cwd()) if project else user_layout()
@@ -0,0 +1,98 @@
1
+ """``skills-lock.json`` — the ``npx skills`` lockfile shape, plus what ``dxs`` needs.
2
+
3
+ Entries ``dxs`` writes carry ``sourceType: "quartermaster"`` and extra keys
4
+ (``ref``, ``commit``, ``cliVersion``, ``installedAt``, ``agents``,
5
+ ``linkMode``). Entries written by other installers are preserved verbatim and
6
+ never managed.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ SOURCE = "datex/skills"
17
+ SOURCE_TYPE = "quartermaster"
18
+ LOCK_VERSION = 1
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class LockEntry:
23
+ name: str
24
+ skill_path: str
25
+ computed_hash: str
26
+ ref: str
27
+ commit: str
28
+ cli_version: str
29
+ installed_at: str
30
+ agents: tuple[str, ...]
31
+ link_mode: str # "symlink" | "copy"
32
+
33
+ def to_dict(self) -> dict[str, Any]:
34
+ return {
35
+ "source": SOURCE,
36
+ "sourceType": SOURCE_TYPE,
37
+ "skillPath": self.skill_path,
38
+ "computedHash": self.computed_hash,
39
+ "ref": self.ref,
40
+ "commit": self.commit,
41
+ "cliVersion": self.cli_version,
42
+ "installedAt": self.installed_at,
43
+ "agents": list(self.agents),
44
+ "linkMode": self.link_mode,
45
+ }
46
+
47
+ @classmethod
48
+ def from_dict(cls, name: str, raw: dict[str, Any]) -> LockEntry:
49
+ return cls(
50
+ name=name,
51
+ skill_path=str(raw.get("skillPath", "")),
52
+ computed_hash=str(raw.get("computedHash", "")),
53
+ ref=str(raw.get("ref", "")),
54
+ commit=str(raw.get("commit", "")),
55
+ cli_version=str(raw.get("cliVersion", "")),
56
+ installed_at=str(raw.get("installedAt", "")),
57
+ agents=tuple(str(a) for a in raw.get("agents", [])),
58
+ link_mode=str(raw.get("linkMode", "symlink")),
59
+ )
60
+
61
+
62
+ @dataclass
63
+ class Lock:
64
+ skills: dict[str, LockEntry] = field(default_factory=dict)
65
+ foreign: dict[str, Any] = field(default_factory=dict)
66
+ extra: dict[str, Any] = field(default_factory=dict)
67
+
68
+ def managed(self) -> dict[str, LockEntry]:
69
+ return dict(self.skills)
70
+
71
+
72
+ def read_lock(path: Path) -> Lock:
73
+ if not path.is_file():
74
+ return Lock()
75
+ try:
76
+ raw = json.loads(path.read_text(encoding="utf-8"))
77
+ except (OSError, ValueError):
78
+ return Lock()
79
+ if not isinstance(raw, dict):
80
+ return Lock()
81
+ lock = Lock(extra={k: v for k, v in raw.items() if k not in ("version", "skills")})
82
+ entries = raw.get("skills")
83
+ if isinstance(entries, dict):
84
+ for name, entry in entries.items():
85
+ if isinstance(entry, dict) and entry.get("sourceType") == SOURCE_TYPE:
86
+ lock.skills[name] = LockEntry.from_dict(name, entry)
87
+ else:
88
+ lock.foreign[name] = entry
89
+ return lock
90
+
91
+
92
+ def write_lock(path: Path, lock: Lock) -> None:
93
+ skills: dict[str, Any] = dict(lock.foreign)
94
+ for name in sorted(lock.skills):
95
+ skills[name] = lock.skills[name].to_dict()
96
+ doc: dict[str, Any] = {"version": LOCK_VERSION, **lock.extra, "skills": skills}
97
+ path.parent.mkdir(parents=True, exist_ok=True)
98
+ path.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8")