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