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.
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/ci.yml +29 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/PKG-INFO +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/pyproject.toml +3 -3
- datex_studio_cli-0.5.2/scripts/check_wheel_completeness.py +58 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/__init__.py +1 -1
- datex_studio_cli-0.5.2/src/dxs/core/quartermaster/__init__.py +5 -0
- datex_studio_cli-0.5.2/src/dxs/core/quartermaster/client.py +178 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/__init__.py +1 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/frontmatter.py +30 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/installer.py +148 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/layout.py +57 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/lock.py +98 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/resolver.py +48 -0
- datex_studio_cli-0.5.2/src/dxs/core/skills/staleness.py +55 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/404/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/404.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/capture/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/capture/index.txt +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/design/index.txt +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/index.txt +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.dockerignore +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/actionlint.yaml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/build-image.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/promote-prod.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.github/workflows/release.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/.gitignore +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/CONTEXT.md +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/README.md +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/.env.example +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/Dockerfile +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/README.md +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/apply-local-cache.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/collector-config.yaml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/docker-compose.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/local-test-settings.json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/otel/remote-settings.json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/report-creator.skill +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/datadog_errors.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/scripts/wavelength_query.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/cli.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/auth.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/branch_commit_guard.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/cascade.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/crm_delivery.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/crm_time.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/init.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/_common.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/api.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/create.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/data.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/pack.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/ng/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/reference.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/reset.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/skills.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/commands/validation_report.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/agent_browser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/app_config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/reference_graph.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/api/replacements.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/datex_tenant.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/msal_client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/tenants.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/flow_shape.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/html.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/pagination.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/resolvers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/dynamics/time_entry.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/onboarding.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/folder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/manifest.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/mocks.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/scaffold.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/ng/serve_lock.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/py.typed +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/clipboard.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/utils/versioning.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/proxy_app.py +0 -0
- {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
- {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
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/538-b06224e65f71c9a1.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {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
- {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
- {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
- {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
- {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
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {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
- {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
- {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
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.2}/src/dxs/web/static/report-preview.html +0 -0
- {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.
|
|
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.
|
|
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())
|
|
@@ -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")
|