ixt-cli 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/.gitignore +1 -0
  2. ixt_cli-0.9.0/LICENSE +21 -0
  3. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/PKG-INFO +52 -45
  4. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/README.md +49 -43
  5. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/pyproject.toml +2 -6
  6. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/backends/binary.py +71 -19
  7. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_apply.py +54 -7
  8. ixt_cli-0.9.0/src/ixt/cli/cmd_lock.py +89 -0
  9. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/commands.py +3 -0
  10. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/parser.py +28 -0
  11. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/heuristics.py +47 -1
  12. ixt_cli-0.9.0/src/ixt/config/lockfile.py +279 -0
  13. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/apply.py +148 -6
  14. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/discover.py +84 -1
  15. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/install.py +4 -0
  16. ixt_cli-0.9.0/src/ixt/core/lockfile.py +504 -0
  17. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/data/heuristics.toml +1 -1
  18. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/npm.py +18 -2
  19. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/pypi.py +12 -0
  20. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_apply.py +372 -2
  21. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_argparse_completion.py +1 -1
  22. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_binary_backend.py +193 -18
  23. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_cli.py +9 -0
  24. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_discover.py +80 -0
  25. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_heuristics.py +33 -0
  26. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_install.py +1 -1
  27. ixt_cli-0.9.0/tests/unit/test_lock_write.py +561 -0
  28. ixt_cli-0.9.0/tests/unit/test_lockfile.py +195 -0
  29. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/__init__.py +0 -0
  30. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/__main__.py +0 -0
  31. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/backends/__init__.py +0 -0
  32. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/backends/binary_resolver.py +0 -0
  33. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/backends/node.py +0 -0
  34. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/backends/python.py +0 -0
  35. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/__init__.py +0 -0
  36. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/argparse_completion.py +0 -0
  37. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_cache.py +0 -0
  38. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_config.py +0 -0
  39. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_info.py +0 -0
  40. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_install.py +0 -0
  41. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_misc.py +0 -0
  42. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_registry.py +0 -0
  43. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/cmd_upgrade.py +0 -0
  44. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/cli/render.py +0 -0
  45. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/__init__.py +0 -0
  46. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/asset_index.py +0 -0
  47. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/asset_pattern_cache.py +0 -0
  48. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/env_policy.py +0 -0
  49. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/flags.py +0 -0
  50. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/fs_policy.py +0 -0
  51. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/models.py +0 -0
  52. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/registry.py +0 -0
  53. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/settings.py +0 -0
  54. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/setup_toml.py +0 -0
  55. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/config/toml.py +0 -0
  56. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/__init__.py +0 -0
  57. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/apply_actions.py +0 -0
  58. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/backend.py +0 -0
  59. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/bootstrap.py +0 -0
  60. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/cache.py +0 -0
  61. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/doctor.py +0 -0
  62. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/export.py +0 -0
  63. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/expose.py +0 -0
  64. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/extract.py +0 -0
  65. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/hooks.py +0 -0
  66. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/identity.py +0 -0
  67. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/inject.py +0 -0
  68. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/install_local.py +0 -0
  69. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/locks.py +0 -0
  70. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/pathlink.py +0 -0
  71. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/resolution_stats.py +0 -0
  72. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/resolve.py +0 -0
  73. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/resolve_cache.py +0 -0
  74. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/runtimes.py +0 -0
  75. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/save.py +0 -0
  76. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/setup_completions.py +0 -0
  77. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/setup_path.py +0 -0
  78. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/core/upgrade.py +0 -0
  79. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/data/__init__.py +0 -0
  80. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/data/asset_index.json +0 -0
  81. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/data/registry.toml +0 -0
  82. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/__init__.py +0 -0
  83. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/constants.py +0 -0
  84. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/fmt.py +0 -0
  85. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/logger.py +0 -0
  86. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/output.py +0 -0
  87. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/req_spec.py +0 -0
  88. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/semver.py +0 -0
  89. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/shell.py +0 -0
  90. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/libs/style.py +0 -0
  91. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/__init__.py +0 -0
  92. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/github_api.py +0 -0
  93. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/gitlab_api.py +0 -0
  94. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/http.py +0 -0
  95. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/net/source.py +0 -0
  96. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/platform/__init__.py +0 -0
  97. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/src/ixt/platform/win.py +0 -0
  98. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/__init__.py +0 -0
  99. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/conftest.py +0 -0
  100. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/README.md +0 -0
  101. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/conftest.py +0 -0
  102. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/docker/Dockerfile.alpine +0 -0
  103. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/docker/Dockerfile.fedora +0 -0
  104. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/docker/Dockerfile.ubuntu +0 -0
  105. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/playground_incus.sh +0 -0
  106. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/run_e2e_docker.sh +0 -0
  107. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/run_e2e_incus.sh +0 -0
  108. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/runtime_matrix.sh +0 -0
  109. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/e2e/test_e2e.py +0 -0
  110. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/integration/__init__.py +0 -0
  111. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/integration/test_lifecycle.py +0 -0
  112. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/integration/test_policy_runtime.py +0 -0
  113. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/__init__.py +0 -0
  114. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_asset_index.py +0 -0
  115. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_asset_pattern_cache.py +0 -0
  116. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_backend.py +0 -0
  117. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_bootstrap.py +0 -0
  118. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_cache.py +0 -0
  119. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_doctor.py +0 -0
  120. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_env_policy.py +0 -0
  121. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_export.py +0 -0
  122. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_expose.py +0 -0
  123. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_extract.py +0 -0
  124. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_flags.py +0 -0
  125. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_fs_policy.py +0 -0
  126. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_github_api.py +0 -0
  127. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_gitlab_api.py +0 -0
  128. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_hooks.py +0 -0
  129. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_http.py +0 -0
  130. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_inject.py +0 -0
  131. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_install_local.py +0 -0
  132. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_models.py +0 -0
  133. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_namespacing.py +0 -0
  134. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_node_backend.py +0 -0
  135. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_output.py +0 -0
  136. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_pathlink.py +0 -0
  137. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_platform.py +0 -0
  138. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_python_backend.py +0 -0
  139. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_registry.py +0 -0
  140. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_req_spec.py +0 -0
  141. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_resolve.py +0 -0
  142. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_resolve_cache.py +0 -0
  143. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_runtimes.py +0 -0
  144. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_save.py +0 -0
  145. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_semver.py +0 -0
  146. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_settings.py +0 -0
  147. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_setup_path.py +0 -0
  148. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_setup_toml.py +0 -0
  149. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_source.py +0 -0
  150. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_style.py +0 -0
  151. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_toml.py +0 -0
  152. {ixt_cli-0.8.0 → ixt_cli-0.9.0}/tests/unit/test_upgrade.py +0 -0
@@ -150,3 +150,4 @@ ixt.egg-info
150
150
 
151
151
  .memsearch/
152
152
  .pyscn/
153
+ .pi/
ixt_cli-0.9.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gaëtan Montury
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ixt-cli
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Universal CLI tool isolator - manage Python, Node, and binary tools with per-tool isolation
5
5
  Project-URL: Homepage, https://gitlab.com/pytgaen-group/ixt
6
6
  Project-URL: Repository, https://gitlab.com/pytgaen-group/ixt.git
7
7
  Project-URL: Issues, https://gitlab.com/pytgaen-group/ixt/-/issues
8
- Project-URL: Documentation, https://gitlab.com/pytgaen-group/ixt#readme
8
+ Project-URL: Documentation, https://ixt-39f604.gitlab.io
9
9
  Author-email: Gaëtan Montury <10528250-pytgaen@users.noreply.gitlab.com>
10
10
  License: MIT
11
+ License-File: LICENSE
11
12
  Keywords: cli,github,isolation,npm,package-manager,pypi,tool
12
13
  Classifier: Development Status :: 4 - Beta
13
14
  Classifier: Environment :: Console
@@ -27,30 +28,37 @@ Requires-Python: >=3.10
27
28
  Description-Content-Type: text/markdown
28
29
 
29
30
  <p align="center">
30
- <img src="assets/ixt.jpg" alt="ixt" width="600">
31
+ <img src="https://ixt-39f604.gitlab.io/assets/ixt.jpg" alt="ixt" width="600">
31
32
  </p>
32
33
 
33
34
  <h1 align="center">ixt — Universal CLI Tool Isolator</h1>
34
35
 
35
36
  <p align="center">
36
- <a href="https://gitlab.com/pytgaen-group/ixt/-/releases"><img src="https://img.shields.io/gitlab/v/release/pytgaen-group/ixt?label=release" alt="Release"></a>
37
- <a href="https://gitlab.com/pytgaen-group/ixt/-/pipelines"><img src="https://img.shields.io/gitlab/pipeline-status/pytgaen-group/ixt?branch=main" alt="Pipeline"></a>
37
+ <a href="https://gitlab.com/pytgaen-group/ixt/-/tags"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F81169049%2Frepository%2Ftags%3Fper_page%3D1&amp;query=%24%5B0%5D.name&amp;label=release" alt="Release"></a>
38
+ <a href="https://gitlab.com/pytgaen-group/ixt/-/blob/main/.gitlab-ci.yml"><img src="https://img.shields.io/badge/build-GitLab%20CI-blue?logo=gitlab" alt="Build: GitLab CI"></a>
39
+ <a href="https://pypi.org/project/ixt-cli/"><img src="https://img.shields.io/pypi/v/ixt-cli?label=pypi" alt="PyPI"></a>
40
+ <a href="https://pepy.tech/projects/ixt-cli"><img src="https://static.pepy.tech/badge/ixt-cli/month" alt="PyPI Downloads"></a>
38
41
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
39
42
  <img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
40
43
  </p>
41
44
 
42
45
  <p align="center">
43
- <img src="assets/wip.jpg" alt="Work in Progress" width="300">
46
+ <img src="https://ixt-39f604.gitlab.io/assets/wip.jpg" alt="Work in Progress" width="300">
44
47
  </p>
45
48
 
46
49
  **One CLI for Python, Node, and binary tools — each in its own isolated env.**
47
- **Optional Linux runtime policy can restrict env vars and filesystem access per tool.**
48
50
 
49
51
  *Zero third-party deps. Zero system pollution. One command, any ecosystem.*
50
52
 
53
+ - **Day-one GitHub releases** — install a tool the day it ships, without
54
+ waiting for a formula, registry entry, or plugin.
55
+ - **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
56
+ bootstrapped on first use, so the host does not need Node, npm, or a package
57
+ manager pre-installed.
58
+
51
59
  ```bash
52
60
  ixt tool install ruff # Python (PyPI)
53
- ixt tool install @anthropic-ai/claude-code # Node (npm)
61
+ ixt tool install @anthropic-ai/claude-code # npm package via bun
54
62
  ixt tool install ripgrep # Binary (GitHub — via built-in registry)
55
63
  ```
56
64
 
@@ -62,10 +70,10 @@ From PyPI. The published package is `ixt-cli`; it installs the `ixt` command.
62
70
 
63
71
  ```bash
64
72
  # One-shot via uvx (requires uv)
65
- uvx --from ixt-cli==0.7.0 ixt --help
73
+ uvx --from ixt-cli ixt --help
66
74
 
67
75
  # Persistent install via uv tool
68
- uv tool install ixt-cli==0.7.0
76
+ uv tool install ixt-cli
69
77
  ```
70
78
 
71
79
  Or from source:
@@ -83,16 +91,16 @@ ixt setup path
83
91
 
84
92
  ### Integrity verification
85
93
 
86
- Release tags are SSH-signed. Installing from PyPI pins the published package
87
- version but does not verify the Git tag signature locally. Use the clone flow
88
- when you want to verify the signed release before installing:
94
+ Release tags are SSH-signed. Installing from PyPI does not verify the Git tag
95
+ signature locally. Use the clone flow when you want to verify a signed release
96
+ before installing:
89
97
 
90
98
  ```bash
91
99
  # Verify the signed tag (requires the maintainer's SSH public key in git's allowed_signers)
92
100
  git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
93
101
  git fetch --tags
94
- git verify-tag v0.7.0
95
- git checkout --detach v0.7.0
102
+ git verify-tag vX.Y.Z
103
+ git checkout --detach vX.Y.Z
96
104
  uv tool install .
97
105
  ```
98
106
 
@@ -144,30 +152,6 @@ ixt tool install @npm:prettier
144
152
  ixt tool install @gh:BurntSushi/ripgrep
145
153
  ```
146
154
 
147
- ## 🛡️ Runtime policy
148
-
149
- Dependency isolation is the baseline — but ixt also lets you control **what each tool can see at runtime**: which environment variables it receives, which filesystem paths it can read or write. This is a runtime policy for commands launched through ixt shims, not a sandbox for install-time package-manager scripts. If the install step itself is untrusted, use a disposable VM/container/user.
150
-
151
- ```bash
152
- # Avoid passing secrets in the runtime environment
153
- ixt tool config some-cli env base os-common
154
- ixt tool config some-cli env allow '*RUFF*'
155
-
156
- # Restrict filesystem at runtime: read-only host, hide ~/.aws and ~/.ssh
157
- ixt tool config some-cli fs base app-common
158
- ixt tool config some-cli fs scratch ~/.aws ~/.ssh
159
- ```
160
-
161
- When a policy is active, the PATH shim becomes a small Python wrapper that — when [bubblewrap](https://github.com/containers/bubblewrap) is installed — runs the tool with PID isolation and a private `/proc`. The filtered env reaches the tool, and `/proc/$PPID/environ` reads from inside cannot recover the host secrets. Filesystem policy also requires bwrap. Without bwrap, env policy still cleans the env reaching the tool but `/proc` reads remain exfiltrable — the shim warns once per session in this **degraded "hygiene-only" mode**. Reset the policy and you're back to a direct symlink with zero overhead.
162
-
163
- The policy is replayable: declare `env_base`, `env_allow`, `fs_scratch`, etc. inside `ixt.toml` and `ixt tool apply` reproduces it on any machine. Use `IXT_SHIM_DEBUG=1` to dump the effective env, `IXT_POLICY_QUIET=1` to silence the degraded-mode warning, `IXT_DISABLE_BWRAP=1` to force degraded mode for debugging.
164
-
165
- > ⚠️ **Linux only for now.** Enforcement relies on POSIX `execve` and Linux namespaces. On macOS/Windows the policy is saved to `ixt.json` but **not enforced at runtime** — the tool runs with the full environment.
166
- > Install [bubblewrap](https://github.com/containers/bubblewrap) for **effective env/fs enforcement**: `sudo apt install bubblewrap` / `sudo dnf install bubblewrap` / `sudo pacman -S bubblewrap` / `sudo apk add bubblewrap`. The env axis runs without it but only as a hygiene measure (cleans logs/traces); the fs axis requires it and errors out otherwise. Network isolation is not implemented yet.
167
- > This is not a general malware sandbox.
168
-
169
- [Full policy reference →](https://gitlab.com/pytgaen-group/ixt/-/blob/main/docs/policy/index.md)
170
-
171
155
  ## 📚 Built-in Registry
172
156
 
173
157
  ixt ships with a built-in registry that maps common tool names to their source. No need to remember the GitHub path:
@@ -202,6 +186,24 @@ ixt tool install sharkdp/fd@10 # latest 10.x.x
202
186
  ixt tool install ruff>=0.5.0 # PEP 508 (Python)
203
187
  ```
204
188
 
189
+ ## 🔒 Declarative locks
190
+
191
+ `ixt.toml` is the human-edited intent: which tools you want, with optional
192
+ version bounds, exposure rules, and injections. `ixt.lock` is generated from
193
+ that intent and records the exact resolved versions and binary release assets.
194
+
195
+ For teams and CI, commit both files:
196
+
197
+ ```bash
198
+ ixt lock write
199
+ ixt lock check
200
+ ixt tool apply --locked --strict
201
+ ```
202
+
203
+ `--locked` replays the exact lock instead of resolving newer versions.
204
+ `--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
205
+ diverge.
206
+
205
207
  ## 🧰 Commands
206
208
 
207
209
  | Command | Description |
@@ -215,7 +217,11 @@ ixt tool install ruff>=0.5.0 # PEP 508 (Python)
215
217
  | `ixt tool upgrade <pkg>` | Upgrade a tool |
216
218
  | `ixt tool upgrade --all` | Upgrade all tools |
217
219
  | `ixt tool apply` | Sync tools from declarative config |
220
+ | `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
218
221
  | `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
222
+ | `ixt lock write` | Resolve ixt.toml and write ixt.lock |
223
+ | `ixt lock check` | Fail if ixt.lock is missing or stale |
224
+ | `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
219
225
  | `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
220
226
  | `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
221
227
  | `ixt tool config <pkg> inject <dep>` | Inject a package into the tool's env |
@@ -238,7 +244,8 @@ ixt tool install ruff>=0.5.0 # PEP 508 (Python)
238
244
  ```
239
245
  $IXT_HOME/ # default ~/.local/share/ixt (respects $XDG_DATA_HOME)
240
246
  ├── config/
241
- │ ├── ixt.toml
247
+ │ ├── ixt.toml # global declarative manifest fallback
248
+ │ ├── ixt.lock # global lock when using the global ixt.toml
242
249
  │ ├── registry.toml
243
250
  │ └── heuristics.toml
244
251
  └── installed/
@@ -262,12 +269,12 @@ $IXT_CACHE_HOME/ # default ~/.cache/ixt (respects $XDG_CACHE_HOME)
262
269
  | | uv tool | pipx | npm -g | aqua | mise | proto | **ixt** |
263
270
  | ------------------------ | ------- | ---- | ------ | ---- | --------- | ----- | ------- |
264
271
  | Python tools (PyPI) | ✅ | ✅ | — | — | ✅ (pipx) | — | ✅ |
265
- | Node tools (npm) | — | — | ✅ | — | ✅ (npm) | — | ✅ |
272
+ | npm packages | — | — | ✅ | — | ✅ (npm) | — | ✅ |
266
273
  | GitHub Releases binaries | — | — | — | ✅ | ✅ (ubi) | ~ | ✅ |
274
+ | Day-one GitHub releases | — | — | — | ❌ | ❌ | ~ | ✅ |
267
275
  | Dependency isolation | ✅ | ✅ | ❌ | — | partial | — | ✅ |
268
276
  | Fine-grained exposure | — | — | — | — | — | — | ✅ |
269
- | Runtime policy (env+fs) | — | — | — | — | — | — | ✅ |
270
- | Bootstrap from scratch | — | — | — | — | curl | curl | ✅ |
277
+ | No system runtime needed | — | — | ❌ | ❌ | ❌ | ❌ | ✅ |
271
278
  | Declarative config | — | — | — | YAML | TOML | TOML | TOML |
272
279
 
273
280
  ixt is **not** a runtime version manager (mise, proto, asdf cover that). It manages the **last mile**: install, isolate, expose and group CLI tools across three ecosystems.
@@ -285,10 +292,10 @@ uv run ruff format src/ tests/ # format
285
292
 
286
293
  ## 🤝 Contributing
287
294
 
288
- Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>. The GitHub repository (if present) is a **read-only mirror** — please open issues and merge requests on GitLab.
295
+ Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>. Any GitHub repository for this project is a **read-only mirror** of GitLab; it is not the review or issue tracker. Please open issues and merge requests on GitLab.
289
296
 
290
297
  Bug reports and reproduction recipes are welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details, or open an issue to start a discussion.
291
298
 
292
299
  ## 📄 License
293
300
 
294
- MIT
301
+ MIT. See [`LICENSE`](LICENSE).
@@ -1,28 +1,35 @@
1
1
  <p align="center">
2
- <img src="assets/ixt.jpg" alt="ixt" width="600">
2
+ <img src="https://ixt-39f604.gitlab.io/assets/ixt.jpg" alt="ixt" width="600">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">ixt — Universal CLI Tool Isolator</h1>
6
6
 
7
7
  <p align="center">
8
- <a href="https://gitlab.com/pytgaen-group/ixt/-/releases"><img src="https://img.shields.io/gitlab/v/release/pytgaen-group/ixt?label=release" alt="Release"></a>
9
- <a href="https://gitlab.com/pytgaen-group/ixt/-/pipelines"><img src="https://img.shields.io/gitlab/pipeline-status/pytgaen-group/ixt?branch=main" alt="Pipeline"></a>
8
+ <a href="https://gitlab.com/pytgaen-group/ixt/-/tags"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F81169049%2Frepository%2Ftags%3Fper_page%3D1&amp;query=%24%5B0%5D.name&amp;label=release" alt="Release"></a>
9
+ <a href="https://gitlab.com/pytgaen-group/ixt/-/blob/main/.gitlab-ci.yml"><img src="https://img.shields.io/badge/build-GitLab%20CI-blue?logo=gitlab" alt="Build: GitLab CI"></a>
10
+ <a href="https://pypi.org/project/ixt-cli/"><img src="https://img.shields.io/pypi/v/ixt-cli?label=pypi" alt="PyPI"></a>
11
+ <a href="https://pepy.tech/projects/ixt-cli"><img src="https://static.pepy.tech/badge/ixt-cli/month" alt="PyPI Downloads"></a>
10
12
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
11
13
  <img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
12
14
  </p>
13
15
 
14
16
  <p align="center">
15
- <img src="assets/wip.jpg" alt="Work in Progress" width="300">
17
+ <img src="https://ixt-39f604.gitlab.io/assets/wip.jpg" alt="Work in Progress" width="300">
16
18
  </p>
17
19
 
18
20
  **One CLI for Python, Node, and binary tools — each in its own isolated env.**
19
- **Optional Linux runtime policy can restrict env vars and filesystem access per tool.**
20
21
 
21
22
  *Zero third-party deps. Zero system pollution. One command, any ecosystem.*
22
23
 
24
+ - **Day-one GitHub releases** — install a tool the day it ships, without
25
+ waiting for a formula, registry entry, or plugin.
26
+ - **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
27
+ bootstrapped on first use, so the host does not need Node, npm, or a package
28
+ manager pre-installed.
29
+
23
30
  ```bash
24
31
  ixt tool install ruff # Python (PyPI)
25
- ixt tool install @anthropic-ai/claude-code # Node (npm)
32
+ ixt tool install @anthropic-ai/claude-code # npm package via bun
26
33
  ixt tool install ripgrep # Binary (GitHub — via built-in registry)
27
34
  ```
28
35
 
@@ -34,10 +41,10 @@ From PyPI. The published package is `ixt-cli`; it installs the `ixt` command.
34
41
 
35
42
  ```bash
36
43
  # One-shot via uvx (requires uv)
37
- uvx --from ixt-cli==0.7.0 ixt --help
44
+ uvx --from ixt-cli ixt --help
38
45
 
39
46
  # Persistent install via uv tool
40
- uv tool install ixt-cli==0.7.0
47
+ uv tool install ixt-cli
41
48
  ```
42
49
 
43
50
  Or from source:
@@ -55,16 +62,16 @@ ixt setup path
55
62
 
56
63
  ### Integrity verification
57
64
 
58
- Release tags are SSH-signed. Installing from PyPI pins the published package
59
- version but does not verify the Git tag signature locally. Use the clone flow
60
- when you want to verify the signed release before installing:
65
+ Release tags are SSH-signed. Installing from PyPI does not verify the Git tag
66
+ signature locally. Use the clone flow when you want to verify a signed release
67
+ before installing:
61
68
 
62
69
  ```bash
63
70
  # Verify the signed tag (requires the maintainer's SSH public key in git's allowed_signers)
64
71
  git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
65
72
  git fetch --tags
66
- git verify-tag v0.7.0
67
- git checkout --detach v0.7.0
73
+ git verify-tag vX.Y.Z
74
+ git checkout --detach vX.Y.Z
68
75
  uv tool install .
69
76
  ```
70
77
 
@@ -116,30 +123,6 @@ ixt tool install @npm:prettier
116
123
  ixt tool install @gh:BurntSushi/ripgrep
117
124
  ```
118
125
 
119
- ## 🛡️ Runtime policy
120
-
121
- Dependency isolation is the baseline — but ixt also lets you control **what each tool can see at runtime**: which environment variables it receives, which filesystem paths it can read or write. This is a runtime policy for commands launched through ixt shims, not a sandbox for install-time package-manager scripts. If the install step itself is untrusted, use a disposable VM/container/user.
122
-
123
- ```bash
124
- # Avoid passing secrets in the runtime environment
125
- ixt tool config some-cli env base os-common
126
- ixt tool config some-cli env allow '*RUFF*'
127
-
128
- # Restrict filesystem at runtime: read-only host, hide ~/.aws and ~/.ssh
129
- ixt tool config some-cli fs base app-common
130
- ixt tool config some-cli fs scratch ~/.aws ~/.ssh
131
- ```
132
-
133
- When a policy is active, the PATH shim becomes a small Python wrapper that — when [bubblewrap](https://github.com/containers/bubblewrap) is installed — runs the tool with PID isolation and a private `/proc`. The filtered env reaches the tool, and `/proc/$PPID/environ` reads from inside cannot recover the host secrets. Filesystem policy also requires bwrap. Without bwrap, env policy still cleans the env reaching the tool but `/proc` reads remain exfiltrable — the shim warns once per session in this **degraded "hygiene-only" mode**. Reset the policy and you're back to a direct symlink with zero overhead.
134
-
135
- The policy is replayable: declare `env_base`, `env_allow`, `fs_scratch`, etc. inside `ixt.toml` and `ixt tool apply` reproduces it on any machine. Use `IXT_SHIM_DEBUG=1` to dump the effective env, `IXT_POLICY_QUIET=1` to silence the degraded-mode warning, `IXT_DISABLE_BWRAP=1` to force degraded mode for debugging.
136
-
137
- > ⚠️ **Linux only for now.** Enforcement relies on POSIX `execve` and Linux namespaces. On macOS/Windows the policy is saved to `ixt.json` but **not enforced at runtime** — the tool runs with the full environment.
138
- > Install [bubblewrap](https://github.com/containers/bubblewrap) for **effective env/fs enforcement**: `sudo apt install bubblewrap` / `sudo dnf install bubblewrap` / `sudo pacman -S bubblewrap` / `sudo apk add bubblewrap`. The env axis runs without it but only as a hygiene measure (cleans logs/traces); the fs axis requires it and errors out otherwise. Network isolation is not implemented yet.
139
- > This is not a general malware sandbox.
140
-
141
- [Full policy reference →](https://gitlab.com/pytgaen-group/ixt/-/blob/main/docs/policy/index.md)
142
-
143
126
  ## 📚 Built-in Registry
144
127
 
145
128
  ixt ships with a built-in registry that maps common tool names to their source. No need to remember the GitHub path:
@@ -174,6 +157,24 @@ ixt tool install sharkdp/fd@10 # latest 10.x.x
174
157
  ixt tool install ruff>=0.5.0 # PEP 508 (Python)
175
158
  ```
176
159
 
160
+ ## 🔒 Declarative locks
161
+
162
+ `ixt.toml` is the human-edited intent: which tools you want, with optional
163
+ version bounds, exposure rules, and injections. `ixt.lock` is generated from
164
+ that intent and records the exact resolved versions and binary release assets.
165
+
166
+ For teams and CI, commit both files:
167
+
168
+ ```bash
169
+ ixt lock write
170
+ ixt lock check
171
+ ixt tool apply --locked --strict
172
+ ```
173
+
174
+ `--locked` replays the exact lock instead of resolving newer versions.
175
+ `--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
176
+ diverge.
177
+
177
178
  ## 🧰 Commands
178
179
 
179
180
  | Command | Description |
@@ -187,7 +188,11 @@ ixt tool install ruff>=0.5.0 # PEP 508 (Python)
187
188
  | `ixt tool upgrade <pkg>` | Upgrade a tool |
188
189
  | `ixt tool upgrade --all` | Upgrade all tools |
189
190
  | `ixt tool apply` | Sync tools from declarative config |
191
+ | `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
190
192
  | `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
193
+ | `ixt lock write` | Resolve ixt.toml and write ixt.lock |
194
+ | `ixt lock check` | Fail if ixt.lock is missing or stale |
195
+ | `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
191
196
  | `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
192
197
  | `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
193
198
  | `ixt tool config <pkg> inject <dep>` | Inject a package into the tool's env |
@@ -210,7 +215,8 @@ ixt tool install ruff>=0.5.0 # PEP 508 (Python)
210
215
  ```
211
216
  $IXT_HOME/ # default ~/.local/share/ixt (respects $XDG_DATA_HOME)
212
217
  ├── config/
213
- │ ├── ixt.toml
218
+ │ ├── ixt.toml # global declarative manifest fallback
219
+ │ ├── ixt.lock # global lock when using the global ixt.toml
214
220
  │ ├── registry.toml
215
221
  │ └── heuristics.toml
216
222
  └── installed/
@@ -234,12 +240,12 @@ $IXT_CACHE_HOME/ # default ~/.cache/ixt (respects $XDG_CACHE_HOME)
234
240
  | | uv tool | pipx | npm -g | aqua | mise | proto | **ixt** |
235
241
  | ------------------------ | ------- | ---- | ------ | ---- | --------- | ----- | ------- |
236
242
  | Python tools (PyPI) | ✅ | ✅ | — | — | ✅ (pipx) | — | ✅ |
237
- | Node tools (npm) | — | — | ✅ | — | ✅ (npm) | — | ✅ |
243
+ | npm packages | — | — | ✅ | — | ✅ (npm) | — | ✅ |
238
244
  | GitHub Releases binaries | — | — | — | ✅ | ✅ (ubi) | ~ | ✅ |
245
+ | Day-one GitHub releases | — | — | — | ❌ | ❌ | ~ | ✅ |
239
246
  | Dependency isolation | ✅ | ✅ | ❌ | — | partial | — | ✅ |
240
247
  | Fine-grained exposure | — | — | — | — | — | — | ✅ |
241
- | Runtime policy (env+fs) | — | — | — | — | — | — | ✅ |
242
- | Bootstrap from scratch | — | — | — | — | curl | curl | ✅ |
248
+ | No system runtime needed | — | — | ❌ | ❌ | ❌ | ❌ | ✅ |
243
249
  | Declarative config | — | — | — | YAML | TOML | TOML | TOML |
244
250
 
245
251
  ixt is **not** a runtime version manager (mise, proto, asdf cover that). It manages the **last mile**: install, isolate, expose and group CLI tools across three ecosystems.
@@ -257,10 +263,10 @@ uv run ruff format src/ tests/ # format
257
263
 
258
264
  ## 🤝 Contributing
259
265
 
260
- Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>. The GitHub repository (if present) is a **read-only mirror** — please open issues and merge requests on GitLab.
266
+ Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>. Any GitHub repository for this project is a **read-only mirror** of GitLab; it is not the review or issue tracker. Please open issues and merge requests on GitLab.
261
267
 
262
268
  Bug reports and reproduction recipes are welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details, or open an issue to start a discussion.
263
269
 
264
270
  ## 📄 License
265
271
 
266
- MIT
272
+ MIT. See [`LICENSE`](LICENSE).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ixt-cli"
7
- version = "0.8.0" # to bump
7
+ version = "0.9.0" # to bump
8
8
  description = "Universal CLI tool isolator - manage Python, Node, and binary tools with per-tool isolation"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -37,10 +37,6 @@ dev = [
37
37
  "pytest-cov>=4.1",
38
38
  "pyright>=1.1",
39
39
  ]
40
- # Standalone binary build (Nuitka). Not a runtime dependency — install with
41
- # `uv sync --group build`. [onefile] pulls zstandard for payload compression.
42
- # See notes/standalone-build.md.
43
- build = ["nuitka[onefile]>=2.5"]
44
40
 
45
41
  [project.scripts]
46
42
  ixt = "ixt.cli:main"
@@ -49,7 +45,7 @@ ixt = "ixt.cli:main"
49
45
  Homepage = "https://gitlab.com/pytgaen-group/ixt"
50
46
  Repository = "https://gitlab.com/pytgaen-group/ixt.git"
51
47
  Issues = "https://gitlab.com/pytgaen-group/ixt/-/issues"
52
- Documentation = "https://gitlab.com/pytgaen-group/ixt#readme"
48
+ Documentation = "https://ixt-39f604.gitlab.io"
53
49
 
54
50
  [tool.hatch.build.targets.wheel]
55
51
  packages = ["src/ixt"]
@@ -23,6 +23,7 @@ from ixt.config.heuristics import (
23
23
  )
24
24
 
25
25
  if TYPE_CHECKING:
26
+ from ixt.config.lockfile import BinaryLockMetadata
26
27
  from ixt.config.setup_toml import SetupConfig
27
28
  from ixt.config.settings import Settings, get_settings
28
29
  from ixt.core.backend import BackendType
@@ -30,10 +31,12 @@ from ixt.core.discover import find_executables
30
31
  from ixt.core.extract import extract, is_archive
31
32
  from ixt.libs.logger import get_logger, get_verbosity
32
33
  from ixt.net.source import Asset, Release, ReleaseSource, RepoSpec, parse_spec
34
+ from ixt.platform import get_arch, get_os
33
35
 
34
36
  __all__ = [
35
37
  "AssetSelectionError",
36
38
  "BinaryBackend",
39
+ "BinaryPlatformError",
37
40
  "BinarySpecError",
38
41
  "HookEntry",
39
42
  "SetupMetadata",
@@ -89,6 +92,31 @@ class AssetSelectionError(RuntimeError):
89
92
  )
90
93
 
91
94
 
95
+ class BinaryPlatformError(RuntimeError):
96
+ """Raised when extracted executables are incompatible with the current platform."""
97
+
98
+ def __init__(
99
+ self,
100
+ spec: str,
101
+ release_tag: str,
102
+ asset_name: str,
103
+ rejected: list[tuple[Path, str]],
104
+ ):
105
+ self.spec = spec
106
+ self.release_tag = release_tag
107
+ self.asset_name = asset_name
108
+ self.rejected = rejected
109
+ platform = f"{get_os().value}/{get_arch().value}"
110
+ details = ", ".join(
111
+ f"{path.name} ({format_label})"
112
+ for path, format_label in sorted(rejected, key=lambda item: str(item[0]))
113
+ )
114
+ super().__init__(
115
+ f"No executable compatible with {platform} found for '{spec}' "
116
+ f"(release {release_tag}, asset {asset_name}). Rejected: {details}"
117
+ )
118
+
119
+
92
120
  class BinarySpecError(ValueError):
93
121
  """Raised when a spec cannot be parsed as a binary backend spec."""
94
122
 
@@ -396,6 +424,7 @@ class BinaryBackend:
396
424
  setup_path: Path | None = None,
397
425
  asset_pattern: str | None = None,
398
426
  known_tag: str | None = None,
427
+ locked_binary: BinaryLockMetadata | None = None,
399
428
  use_cache: bool = True,
400
429
  ) -> dict | None:
401
430
  """Compatibility adapter for the shared backend protocol."""
@@ -410,6 +439,7 @@ class BinaryBackend:
410
439
  setup_path=setup_path,
411
440
  asset_pattern=asset_pattern,
412
441
  known_tag=known_tag,
442
+ locked_binary=locked_binary,
413
443
  use_cache=use_cache,
414
444
  )
415
445
 
@@ -422,6 +452,7 @@ class BinaryBackend:
422
452
  setup_path: Path | None = None,
423
453
  asset_pattern: str | None = None,
424
454
  known_tag: str | None = None,
455
+ locked_binary: BinaryLockMetadata | None = None,
425
456
  use_cache: bool = True,
426
457
  ) -> dict | None:
427
458
  """Resolve, download, extract, and discover one atomic binary tool.
@@ -460,24 +491,34 @@ class BinaryBackend:
460
491
  )
461
492
  release_pattern_name = setup.name if setup and setup.name else None
462
493
 
463
- # Reuse the release already fetched during version resolution (e.g. an
464
- # upgrade or a dry-run within the TTL window). This skips a second API
465
- # call — the only option for GitLab, whose asset URLs can't be rebuilt
466
- # from a pattern like GitHub's.
467
- release = self._cached_release(spec) if use_cache and not repo_spec.version else None
468
- if release is not None:
469
- log.debug(f"release (resolve cache): {release.tag} ({len(release.assets)} assets)")
470
- else:
471
- release = self._fetch_release(
472
- source,
473
- repo_spec,
474
- env_dir=env_dir,
475
- upgrade=upgrade,
476
- known_tag=known_tag,
477
- asset_pattern=release_pattern,
478
- pattern_name=release_pattern_name,
494
+ if locked_binary is not None:
495
+ if not repo_spec.version:
496
+ raise BinarySpecError("locked binary install requires a pinned release tag")
497
+ release = Release(
498
+ tag=repo_spec.version,
499
+ assets=[Asset(name=locked_binary.asset, url=locked_binary.url)],
500
+ resolution_source="lockfile",
479
501
  )
480
- log.debug(f"release: {release.tag} ({len(release.assets)} assets)")
502
+ log.debug(f"release (lockfile): {release.tag} ({locked_binary.asset})")
503
+ else:
504
+ # Reuse the release already fetched during version resolution (e.g. an
505
+ # upgrade or a dry-run within the TTL window). This skips a second API
506
+ # call — the only option for GitLab, whose asset URLs can't be rebuilt
507
+ # from a pattern like GitHub's.
508
+ release = self._cached_release(spec) if use_cache and not repo_spec.version else None
509
+ if release is not None:
510
+ log.debug(f"release (resolve cache): {release.tag} ({len(release.assets)} assets)")
511
+ else:
512
+ release = self._fetch_release(
513
+ source,
514
+ repo_spec,
515
+ env_dir=env_dir,
516
+ upgrade=upgrade,
517
+ known_tag=known_tag,
518
+ asset_pattern=release_pattern,
519
+ pattern_name=release_pattern_name,
520
+ )
521
+ log.debug(f"release: {release.tag} ({len(release.assets)} assets)")
481
522
 
482
523
  if setup is None and not setup_resolved:
483
524
  setup = self._resolve_setup(
@@ -491,7 +532,10 @@ class BinaryBackend:
491
532
  if asset_pattern:
492
533
  log.debug(f"user-forced asset_pattern: {asset_pattern!r}")
493
534
  selection_pattern = _selection_pattern(effective_pattern, release.tag)
494
- asset = self._select_asset(release, selection_pattern, tool_name, spec)
535
+ if locked_binary is None:
536
+ asset = self._select_asset(release, selection_pattern, tool_name, spec)
537
+ else:
538
+ asset = release.assets[0]
495
539
  log.debug(f"selected asset: {asset.name}")
496
540
 
497
541
  archive_path = self._download_asset(
@@ -512,8 +556,16 @@ class BinaryBackend:
512
556
  log.debug(f"extracted to: {result.root}")
513
557
 
514
558
  heuristics = load_heuristics()
515
- executables = find_executables(result.root, expose=heuristics.expose, log=log)
559
+ rejected_executables: list[tuple[Path, str]] = []
560
+ executables = find_executables(
561
+ result.root,
562
+ expose=heuristics.expose,
563
+ log=log,
564
+ rejected=rejected_executables,
565
+ )
516
566
  log.debug(f"executables: {[e.name for e in executables]}")
567
+ if not executables and rejected_executables:
568
+ raise BinaryPlatformError(spec, release.tag, asset.name, rejected_executables)
517
569
 
518
570
  binaries = {exe.name: str(exe) for exe in executables}
519
571