ixt-cli 0.9.1__tar.gz → 0.10.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.
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/PKG-INFO +3 -2
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/README.md +2 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/pyproject.toml +1 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/backends/binary.py +136 -18
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/backends/node.py +49 -14
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/__init__.py +9 -0
- ixt_cli-0.10.0/src/ixt/cli/cmd_exec.py +71 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_info.py +5 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_misc.py +24 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/commands.py +3 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/parser.py +37 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/discover.py +27 -4
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/doctor.py +56 -0
- ixt_cli-0.10.0/src/ixt/core/exec_tool.py +165 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/upgrade.py +19 -8
- ixt_cli-0.10.0/src/ixt/libs/elf.py +174 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/http.py +38 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_argparse_completion.py +1 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_binary_backend.py +256 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_cli.py +90 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_discover.py +63 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_doctor.py +94 -0
- ixt_cli-0.10.0/tests/unit/test_elf.py +230 -0
- ixt_cli-0.10.0/tests/unit/test_exec.py +289 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_http.py +49 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_node_backend.py +111 -9
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_upgrade.py +74 -28
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/.gitignore +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/LICENSE +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/__main__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/backends/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/backends/binary_resolver.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/backends/python.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/argparse_completion.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_apply.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_config.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_install.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_lock.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_registry.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/cmd_upgrade.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/cli/render.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/asset_index.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/asset_pattern_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/env_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/flags.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/fs_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/heuristics.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/lockfile.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/models.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/registry.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/settings.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/setup_toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/config/toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/apply.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/apply_actions.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/backend.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/bootstrap.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/export.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/expose.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/extract.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/hooks.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/identity.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/inject.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/install.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/install_local.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/lockfile.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/locks.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/pathlink.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/resolution_stats.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/resolve.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/resolve_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/runtimes.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/save.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/setup_completions.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/setup_path.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/core/transactions.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/data/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/data/asset_index.json +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/data/heuristics.toml +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/data/registry.toml +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/atomic.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/constants.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/fmt.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/logger.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/output.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/req_spec.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/semver.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/shell.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/libs/style.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/github_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/gitlab_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/npm.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/pypi.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/net/source.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/platform/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/src/ixt/platform/win.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/conftest.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/README.md +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/conftest.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/docker/Dockerfile.alpine +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/docker/Dockerfile.fedora +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/docker/Dockerfile.ubuntu +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/playground_incus.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/run_e2e_docker.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/run_e2e_incus.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/runtime_matrix.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/e2e/test_e2e.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/integration/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/integration/test_lifecycle.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/integration/test_policy_runtime.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_apply.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_asset_index.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_asset_pattern_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_atomic.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_backend.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_bootstrap.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_env_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_export.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_expose.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_extract.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_flags.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_fs_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_github_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_gitlab_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_heuristics.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_hooks.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_inject.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_install.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_install_local.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_lock_write.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_lockfile.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_models.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_namespacing.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_output.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_pathlink.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_platform.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_python_backend.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_registry.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_req_spec.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_resolve.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_resolve_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_runtimes.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_save.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_semver.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_settings.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_setup_path.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_setup_toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_source.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_style.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.0}/tests/unit/test_transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ixt-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -223,7 +223,7 @@ resolve upstream again and refresh the lock.
|
|
|
223
223
|
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
224
224
|
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
225
225
|
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
226
|
-
| `ixt lock check` | Validate ixt.lock against ixt.toml locally
|
|
226
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
227
227
|
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
228
228
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
229
229
|
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
@@ -232,6 +232,7 @@ resolve upstream again and refresh the lock.
|
|
|
232
232
|
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
233
233
|
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
234
234
|
| `ixt tool where <pkg>` | Print the tool env path |
|
|
235
|
+
| `ixt exec [--sudo] <tool> -- <args...>` | Run an exposed tool by name, without relying on PATH |
|
|
235
236
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
236
237
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
237
238
|
| `ixt cache info` | Show cache paths and sizes |
|
|
@@ -194,7 +194,7 @@ resolve upstream again and refresh the lock.
|
|
|
194
194
|
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
195
195
|
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
196
196
|
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
197
|
-
| `ixt lock check` | Validate ixt.lock against ixt.toml locally
|
|
197
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
198
198
|
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
199
199
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
200
200
|
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
@@ -203,6 +203,7 @@ resolve upstream again and refresh the lock.
|
|
|
203
203
|
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
204
204
|
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
205
205
|
| `ixt tool where <pkg>` | Print the tool env path |
|
|
206
|
+
| `ixt exec [--sudo] <tool> -- <args...>` | Run an exposed tool by name, without relying on PATH |
|
|
206
207
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
207
208
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
208
209
|
| `ixt cache info` | Show cache paths and sizes |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ixt-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.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"
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from pathlib import Path
|
|
@@ -31,8 +32,10 @@ from ixt.config.settings import Settings, get_settings
|
|
|
31
32
|
from ixt.core.backend import BackendType
|
|
32
33
|
from ixt.core.discover import find_executables
|
|
33
34
|
from ixt.core.extract import extract, is_archive
|
|
35
|
+
from ixt.libs.elf import PREFIX_SIZE as ELF_PREFIX_SIZE
|
|
36
|
+
from ixt.libs.elf import parse_elf, read_elf
|
|
34
37
|
from ixt.libs.logger import get_logger, get_verbosity
|
|
35
|
-
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError
|
|
38
|
+
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError, get_range
|
|
36
39
|
from ixt.net.source import Asset, Release, ReleaseSource, RepoSpec, parse_spec
|
|
37
40
|
from ixt.platform import get_arch, get_os
|
|
38
41
|
|
|
@@ -88,14 +91,27 @@ class SetupMetadata(_SetupMetadataRequired, total=False):
|
|
|
88
91
|
class AssetSelectionError(RuntimeError):
|
|
89
92
|
"""Raised when no suitable asset could be selected for the current platform."""
|
|
90
93
|
|
|
91
|
-
def __init__(
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
spec: str,
|
|
97
|
+
release_tag: str,
|
|
98
|
+
asset_names: list[str],
|
|
99
|
+
unusable: list[tuple[str, str]] | None = None,
|
|
100
|
+
):
|
|
92
101
|
self.spec = spec
|
|
93
102
|
self.release_tag = release_tag
|
|
94
103
|
self.asset_names = asset_names
|
|
104
|
+
self.unusable = unusable or []
|
|
95
105
|
names = ", ".join(asset_names[:10]) or "<none>"
|
|
96
|
-
|
|
106
|
+
message = (
|
|
97
107
|
f"No suitable asset found for '{spec}' (release {release_tag}). Available: {names}"
|
|
98
108
|
)
|
|
109
|
+
if self.unusable:
|
|
110
|
+
skipped = ", ".join(
|
|
111
|
+
f"{name} (needs missing loader {loader})" for name, loader in self.unusable
|
|
112
|
+
)
|
|
113
|
+
message += f". Skipped as unrunnable here: {skipped}"
|
|
114
|
+
super().__init__(message)
|
|
99
115
|
|
|
100
116
|
|
|
101
117
|
class BinaryPlatformError(RuntimeError):
|
|
@@ -580,7 +596,22 @@ class BinaryBackend:
|
|
|
580
596
|
log.debug(f"user-forced asset_pattern: {asset_pattern!r}")
|
|
581
597
|
selection_pattern = _selection_pattern(effective_pattern, release.tag)
|
|
582
598
|
if locked_binary is None:
|
|
583
|
-
|
|
599
|
+
try:
|
|
600
|
+
asset = self._select_asset(
|
|
601
|
+
release, selection_pattern, tool_name, spec, repo_spec=repo_spec
|
|
602
|
+
)
|
|
603
|
+
except AssetSelectionError:
|
|
604
|
+
# A synthesized release carries only the asset its pattern named.
|
|
605
|
+
# If that asset cannot run here, the pattern is wrong for this
|
|
606
|
+
# host, not merely stale — relearn it against the full release.
|
|
607
|
+
relearned = self._relearn_release(source, repo_spec, release)
|
|
608
|
+
if relearned is None:
|
|
609
|
+
raise
|
|
610
|
+
release = relearned
|
|
611
|
+
selection_pattern = _selection_pattern(effective_pattern, release.tag)
|
|
612
|
+
asset = self._select_asset(
|
|
613
|
+
release, selection_pattern, tool_name, spec, repo_spec=repo_spec
|
|
614
|
+
)
|
|
584
615
|
else:
|
|
585
616
|
asset = release.assets[0]
|
|
586
617
|
if not is_asset_arch_compatible(
|
|
@@ -733,23 +764,110 @@ class BinaryBackend:
|
|
|
733
764
|
asset_pattern: str | None,
|
|
734
765
|
tool_name: str,
|
|
735
766
|
main_spec: str,
|
|
767
|
+
*,
|
|
768
|
+
repo_spec: RepoSpec | None = None,
|
|
736
769
|
) -> Asset:
|
|
770
|
+
log = get_logger("binary")
|
|
737
771
|
heuristics = load_heuristics()
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
release.tag,
|
|
750
|
-
[a.name for a in release.assets],
|
|
772
|
+
candidates = list(release.assets)
|
|
773
|
+
unusable: list[tuple[str, str]] = []
|
|
774
|
+
|
|
775
|
+
while candidates:
|
|
776
|
+
asset = select_asset(
|
|
777
|
+
candidates,
|
|
778
|
+
tool_name,
|
|
779
|
+
_strip_v_prefix(release.tag),
|
|
780
|
+
heuristics=heuristics,
|
|
781
|
+
asset_pattern=asset_pattern,
|
|
782
|
+
verbose=get_verbosity(),
|
|
751
783
|
)
|
|
752
|
-
|
|
784
|
+
if asset is None:
|
|
785
|
+
break
|
|
786
|
+
|
|
787
|
+
loader = self._unrunnable_loader(asset, repo_spec=repo_spec, tag=release.tag)
|
|
788
|
+
if loader is None:
|
|
789
|
+
return asset
|
|
790
|
+
|
|
791
|
+
log.debug(f"asset skip: {asset.name} needs missing loader {loader}")
|
|
792
|
+
unusable.append((asset.name, loader))
|
|
793
|
+
candidates = [a for a in candidates if a.name != asset.name]
|
|
794
|
+
|
|
795
|
+
raise AssetSelectionError(
|
|
796
|
+
main_spec,
|
|
797
|
+
release.tag,
|
|
798
|
+
[a.name for a in release.assets],
|
|
799
|
+
unusable,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
def _relearn_release(
|
|
803
|
+
self,
|
|
804
|
+
source: ReleaseSource,
|
|
805
|
+
repo_spec: RepoSpec,
|
|
806
|
+
release: Release,
|
|
807
|
+
) -> Release | None:
|
|
808
|
+
"""Re-fetch *release* from the API after its learned pattern proved wrong.
|
|
809
|
+
|
|
810
|
+
The fast path synthesizes a release holding only the asset its pattern
|
|
811
|
+
names, so a pattern pointing at an unrunnable build leaves selection
|
|
812
|
+
with nothing to fall back on. The pattern is dropped and the real asset
|
|
813
|
+
list fetched, which lets scoring reach the runnable siblings.
|
|
814
|
+
|
|
815
|
+
Returns None when the release already came from the API: there is then
|
|
816
|
+
no wider list to fall back to, and the caller should surface the
|
|
817
|
+
original selection error instead.
|
|
818
|
+
"""
|
|
819
|
+
if (release.resolution_source or "").endswith("-api"):
|
|
820
|
+
return None
|
|
821
|
+
|
|
822
|
+
log = get_logger("binary")
|
|
823
|
+
log.debug(
|
|
824
|
+
f"asset pattern for {repo_spec.owner}/{repo_spec.repo} names an unrunnable "
|
|
825
|
+
"asset — invalidating and refetching from the release API"
|
|
826
|
+
)
|
|
827
|
+
from ixt.config import asset_pattern_cache
|
|
828
|
+
|
|
829
|
+
asset_pattern_cache.invalidate(repo_spec.owner, repo_spec.repo)
|
|
830
|
+
self._require_github_api(repo_spec)
|
|
831
|
+
refetched = source.get_release_by_tag(repo_spec.owner, repo_spec.repo, release.tag)
|
|
832
|
+
refetched.resolution_source = f"{repo_spec.platform}-api"
|
|
833
|
+
log.debug(f"release (relearn): {refetched.tag} ({len(refetched.assets)} assets)")
|
|
834
|
+
return refetched
|
|
835
|
+
|
|
836
|
+
def _unrunnable_loader(
|
|
837
|
+
self,
|
|
838
|
+
asset: Asset,
|
|
839
|
+
*,
|
|
840
|
+
repo_spec: RepoSpec | None,
|
|
841
|
+
tag: str,
|
|
842
|
+
) -> str | None:
|
|
843
|
+
"""Return the loader *asset* needs but this host lacks, else None.
|
|
844
|
+
|
|
845
|
+
A cached asset is read from disk; otherwise only the head of the asset
|
|
846
|
+
is fetched over HTTP Range, which keeps a doomed candidate from costing
|
|
847
|
+
a full download. Only bare binaries can be checked this way — an
|
|
848
|
+
archive reveals nothing at offset zero, so those are validated after
|
|
849
|
+
extraction by ``find_executables`` instead.
|
|
850
|
+
|
|
851
|
+
Any doubt resolves to None: a server without Range support, a network
|
|
852
|
+
hiccup, or a truncated prefix all fall through to the normal path.
|
|
853
|
+
"""
|
|
854
|
+
if os.name == "nt" or is_archive(asset.name):
|
|
855
|
+
return None
|
|
856
|
+
if repo_spec is not None:
|
|
857
|
+
cache_path = _asset_cache_path(self.settings.downloads_dir, repo_spec, asset, tag=tag)
|
|
858
|
+
cached = _cached_asset(cache_path, asset.size)
|
|
859
|
+
if cached is not None:
|
|
860
|
+
# Already on disk: judge the real file, no request needed.
|
|
861
|
+
info = read_elf(cached)
|
|
862
|
+
return info.interpreter if info is not None and info.loader_missing() else None
|
|
863
|
+
|
|
864
|
+
head = get_range(asset.url, ELF_PREFIX_SIZE)
|
|
865
|
+
if head is None:
|
|
866
|
+
return None
|
|
867
|
+
info = parse_elf(head)
|
|
868
|
+
if info is None or not info.loader_missing():
|
|
869
|
+
return None
|
|
870
|
+
return info.interpreter
|
|
753
871
|
|
|
754
872
|
def uninstall_package(self, env_dir: Path, package: str) -> None:
|
|
755
873
|
"""No-op — binary tools are atomic (uninstall removes the whole env)."""
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
|
+
import shlex
|
|
13
14
|
import shutil
|
|
14
15
|
from dataclasses import dataclass, field
|
|
15
16
|
from pathlib import Path
|
|
@@ -27,6 +28,29 @@ from ixt.platform import is_windows
|
|
|
27
28
|
# #!/usr/bin/env nodemon → no match
|
|
28
29
|
_NODE_SHEBANG_RE = re.compile(rb"^#!.*\bnode\b")
|
|
29
30
|
|
|
31
|
+
|
|
32
|
+
def _resolve_bun_path(bun_path: str) -> str:
|
|
33
|
+
"""Return an absolute path without resolving a stable bun symlink."""
|
|
34
|
+
candidate = Path(bun_path).expanduser()
|
|
35
|
+
if not candidate.is_absolute():
|
|
36
|
+
resolved = shutil.which(bun_path)
|
|
37
|
+
if resolved is None:
|
|
38
|
+
raise FileNotFoundError(f"Cannot resolve bun executable: {bun_path}")
|
|
39
|
+
candidate = Path(resolved)
|
|
40
|
+
return str(candidate.absolute())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _bun_shebang(bun_path: str) -> bytes:
|
|
44
|
+
"""Build a portable launcher header for an absolute bun path."""
|
|
45
|
+
if "\n" in bun_path or "\r" in bun_path:
|
|
46
|
+
raise ValueError("bun executable path cannot contain a line break")
|
|
47
|
+
if any(char.isspace() for char in bun_path):
|
|
48
|
+
# macOS splits shebang arguments before ``env -S`` can parse quotes.
|
|
49
|
+
# This second line is a shell exec and a JavaScript no-op/comment.
|
|
50
|
+
return (f'#!/bin/sh\n\':\' //; exec {shlex.quote(bun_path)} "$0" "$@"').encode()
|
|
51
|
+
return f"#!{bun_path}".encode()
|
|
52
|
+
|
|
53
|
+
|
|
30
54
|
# ---------------------------------------------------------------------------
|
|
31
55
|
# npm spec helpers
|
|
32
56
|
# ---------------------------------------------------------------------------
|
|
@@ -190,9 +214,11 @@ def _shim_node_shebangs(env_dir: Path, bun_path: str) -> list[str]:
|
|
|
190
214
|
Returns the list of ``.bin/`` entry names whose real script was
|
|
191
215
|
rewritten (for a user-facing warning).
|
|
192
216
|
"""
|
|
193
|
-
if shutil.which("node"):
|
|
217
|
+
if is_windows() or shutil.which("node"):
|
|
194
218
|
return []
|
|
195
219
|
|
|
220
|
+
bun_path = _resolve_bun_path(bun_path)
|
|
221
|
+
|
|
196
222
|
rewritten: list[str] = []
|
|
197
223
|
bin_dir = _dot_bin_dir(env_dir)
|
|
198
224
|
if not bin_dir.exists():
|
|
@@ -226,7 +252,7 @@ def _shim_node_shebangs(env_dir: Path, bun_path: str) -> list[str]:
|
|
|
226
252
|
|
|
227
253
|
# Break potential hardlink to bun's global cache, then rewrite.
|
|
228
254
|
mode = target.stat().st_mode
|
|
229
|
-
new_content =
|
|
255
|
+
new_content = _bun_shebang(bun_path) + content[nl:]
|
|
230
256
|
target.unlink()
|
|
231
257
|
target.write_bytes(new_content)
|
|
232
258
|
target.chmod(mode)
|
|
@@ -321,10 +347,19 @@ class NodeBackend:
|
|
|
321
347
|
main_spec = specs[0]
|
|
322
348
|
pkg_name, _version = parse_npm_spec(main_spec)
|
|
323
349
|
|
|
324
|
-
used_npm = self._run_install_command(
|
|
350
|
+
used_npm, bun_path = self._run_install_command(
|
|
351
|
+
env_dir, specs, runtime=runtime, upgrade=upgrade
|
|
352
|
+
)
|
|
325
353
|
pkg_data, used_npm = self._verify_and_retry_with_npm(env_dir, pkg_name, specs, used_npm)
|
|
354
|
+
if used_npm:
|
|
355
|
+
bun_path = None
|
|
326
356
|
|
|
327
|
-
self._maybe_shim_node_shebangs(
|
|
357
|
+
self._maybe_shim_node_shebangs(
|
|
358
|
+
env_dir,
|
|
359
|
+
used_npm=used_npm,
|
|
360
|
+
node_shim=node_shim,
|
|
361
|
+
bun_path=bun_path,
|
|
362
|
+
)
|
|
328
363
|
|
|
329
364
|
binaries = {p.stem: str(p) for p in _list_dot_bin(env_dir)}
|
|
330
365
|
|
|
@@ -345,10 +380,10 @@ class NodeBackend:
|
|
|
345
380
|
*,
|
|
346
381
|
runtime: str | None,
|
|
347
382
|
upgrade: bool,
|
|
348
|
-
) -> bool:
|
|
383
|
+
) -> tuple[bool, str | None]:
|
|
349
384
|
"""Run the install via bun (default) or npm (explicit --runtime=node).
|
|
350
385
|
|
|
351
|
-
|
|
386
|
+
Return whether npm was used and the exact bun path used otherwise.
|
|
352
387
|
"""
|
|
353
388
|
log = get_logger("node")
|
|
354
389
|
|
|
@@ -361,14 +396,14 @@ class NodeBackend:
|
|
|
361
396
|
log.debug(f"npm install (--runtime=node): {' '.join(specs)}")
|
|
362
397
|
cmd = ["npm", "install", "--no-fund", "--no-audit", *specs]
|
|
363
398
|
shell_run(cmd, cwd=env_dir, capture_output=True, check=True)
|
|
364
|
-
return True
|
|
399
|
+
return True, None
|
|
365
400
|
|
|
366
401
|
try:
|
|
367
|
-
bun = self._bun()
|
|
402
|
+
bun = _resolve_bun_path(self._bun())
|
|
368
403
|
cmd = [bun, "add", *(["--force"] if upgrade else []), *specs]
|
|
369
404
|
log.debug(f"bun add: {' '.join(specs)}")
|
|
370
405
|
shell_run(cmd, cwd=env_dir, capture_output=True, check=True)
|
|
371
|
-
return False
|
|
406
|
+
return False, bun
|
|
372
407
|
except (ShellError, Exception) as exc:
|
|
373
408
|
if not self._npm_available():
|
|
374
409
|
raise RuntimeError(
|
|
@@ -379,7 +414,7 @@ class NodeBackend:
|
|
|
379
414
|
log.debug(f"bun failed, falling back to npm: {exc}")
|
|
380
415
|
cmd = ["npm", "install", "--no-fund", "--no-audit", *specs]
|
|
381
416
|
shell_run(cmd, cwd=env_dir, capture_output=True, check=True)
|
|
382
|
-
return True
|
|
417
|
+
return True, None
|
|
383
418
|
|
|
384
419
|
def _verify_and_retry_with_npm(
|
|
385
420
|
self,
|
|
@@ -403,18 +438,18 @@ class NodeBackend:
|
|
|
403
438
|
*,
|
|
404
439
|
used_npm: bool,
|
|
405
440
|
node_shim: bool | None,
|
|
441
|
+
bun_path: str | None,
|
|
406
442
|
) -> None:
|
|
407
443
|
"""Rewrite #!node shebangs to bun when bun installed and host has no node.
|
|
408
444
|
|
|
409
445
|
Keeps the tool self-contained without requiring node on the host.
|
|
410
446
|
"""
|
|
411
|
-
if used_npm or node_shim is False:
|
|
447
|
+
if used_npm or node_shim is False or bun_path is None:
|
|
412
448
|
return
|
|
413
449
|
log = get_logger("node")
|
|
414
450
|
try:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
except (OSError, ShellError) as exc:
|
|
451
|
+
rewritten = _shim_node_shebangs(env_dir, bun_path)
|
|
452
|
+
except (OSError, ShellError, ValueError) as exc:
|
|
418
453
|
log.debug(f"node shebang shim skipped: {exc}")
|
|
419
454
|
return
|
|
420
455
|
if rewritten:
|
|
@@ -7,9 +7,18 @@ __all__ = ["main"]
|
|
|
7
7
|
|
|
8
8
|
def main() -> int:
|
|
9
9
|
"""Entry point for ixt CLI."""
|
|
10
|
+
import sys
|
|
11
|
+
|
|
10
12
|
from ixt.cli.commands import COMMANDS
|
|
11
13
|
from ixt.cli.parser import build_parser, normalize_verbosity
|
|
12
14
|
|
|
15
|
+
# 'exec' forwards every argument after the tool name to that tool, including
|
|
16
|
+
# '--help' and '--'. argparse cannot express that, so it never sees them.
|
|
17
|
+
if sys.argv[1:2] == ["exec"]:
|
|
18
|
+
from ixt.cli.cmd_exec import run_exec_argv
|
|
19
|
+
|
|
20
|
+
return run_exec_argv(sys.argv[2:])
|
|
21
|
+
|
|
13
22
|
parser = build_parser()
|
|
14
23
|
args = parser.parse_args()
|
|
15
24
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""``ixt exec`` — run a managed tool by its exposed name."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from ixt.libs.logger import get_logger
|
|
10
|
+
from ixt.libs.style import dim
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ixt.core.exec_tool import ExecError, ExecRequest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _report(exc: ExecError) -> int:
|
|
17
|
+
logger = get_logger("exec")
|
|
18
|
+
logger.error(str(exc))
|
|
19
|
+
if exc.hint:
|
|
20
|
+
logger.error(f" {dim('Hint: ' + exc.hint)}")
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run(request: ExecRequest) -> int:
|
|
25
|
+
from ixt.config.settings import get_settings
|
|
26
|
+
from ixt.core.exec_tool import ExecError, build_exec_plan, run_exec_plan
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
plan = build_exec_plan(request, get_settings().bin_dir)
|
|
30
|
+
if request.dry_run:
|
|
31
|
+
sys.stdout.write(f"{plan.render()}\n")
|
|
32
|
+
return 0
|
|
33
|
+
return run_exec_plan(plan)
|
|
34
|
+
except ExecError as exc:
|
|
35
|
+
return _report(exc)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_exec_argv(argv: list[str]) -> int:
|
|
39
|
+
"""Entry point for the short-circuited ``ixt exec`` path.
|
|
40
|
+
|
|
41
|
+
Called from ``cli.main`` before argparse runs, so that options meant for
|
|
42
|
+
the tool — ``--help`` in particular — are never claimed by ixt.
|
|
43
|
+
"""
|
|
44
|
+
from ixt.core.exec_tool import ExecError, ExecHelpRequested, parse_exec_argv
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
request = parse_exec_argv(argv)
|
|
48
|
+
except ExecHelpRequested:
|
|
49
|
+
from ixt.cli.parser import build_parser
|
|
50
|
+
|
|
51
|
+
# argparse prints the subparser help and exits, as for any other command.
|
|
52
|
+
build_parser().parse_args(["exec", "--help"])
|
|
53
|
+
return 0
|
|
54
|
+
except ExecError as exc:
|
|
55
|
+
return _report(exc)
|
|
56
|
+
|
|
57
|
+
return _run(request)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_exec(args: argparse.Namespace) -> int:
|
|
61
|
+
"""Dispatch entry point, kept so the declared subparser stays reachable."""
|
|
62
|
+
from ixt.core.exec_tool import ExecRequest
|
|
63
|
+
|
|
64
|
+
return _run(
|
|
65
|
+
ExecRequest(
|
|
66
|
+
tool=args.tool,
|
|
67
|
+
tool_args=tuple(getattr(args, "args", ())),
|
|
68
|
+
sudo=getattr(args, "sudo", False),
|
|
69
|
+
dry_run=getattr(args, "dry_run", False),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
@@ -9,9 +9,10 @@ from ixt.cli.render import format_exposed_bin
|
|
|
9
9
|
from ixt.config.models import ToolRecord
|
|
10
10
|
from ixt.config.settings import get_settings
|
|
11
11
|
from ixt.core.install import display_tool_name, resolve_tool_arg
|
|
12
|
+
from ixt.libs.elf import missing_loader
|
|
12
13
|
from ixt.libs.logger import get_logger
|
|
13
14
|
from ixt.libs.output import data
|
|
14
|
-
from ixt.libs.style import bold, cyan, dim
|
|
15
|
+
from ixt.libs.style import bold, cyan, dim, yellow
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _load_binary_metadata(env_dir: Path) -> dict | None:
|
|
@@ -110,6 +111,9 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
110
111
|
data(" Exposed:")
|
|
111
112
|
for name, source in sorted(record.exposed_bins.items()):
|
|
112
113
|
data(f" {cyan(name)} {dim('->')} {dim(source)}")
|
|
114
|
+
loader = missing_loader(Path(source))
|
|
115
|
+
if loader:
|
|
116
|
+
data(f" {yellow(f'! not runnable: missing loader {loader}')}")
|
|
113
117
|
else:
|
|
114
118
|
data(f" Exposed: {dim('none')}")
|
|
115
119
|
|
|
@@ -163,9 +163,32 @@ def cmd_doctor(args: argparse.Namespace) -> int:
|
|
|
163
163
|
logger.info("")
|
|
164
164
|
if report.ok:
|
|
165
165
|
logger.info(f" {green('All checks passed')}")
|
|
166
|
-
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
fixable = report.fixable
|
|
169
|
+
if not getattr(args, "fix", False):
|
|
167
170
|
n = len(report.warnings)
|
|
168
171
|
logger.info(f" {yellow(f'{n} warning(s)')}")
|
|
172
|
+
if fixable:
|
|
173
|
+
logger.info(f" {dim(f'{len(fixable)} can be repaired with: ixt doctor --fix')}")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
if not fixable:
|
|
177
|
+
n = len(report.warnings)
|
|
178
|
+
logger.info(f" {yellow(f'{n} warning(s)')} {dim('— none of them have an automatic fix')}")
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
for check in fixable:
|
|
182
|
+
assert check.fix is not None
|
|
183
|
+
try:
|
|
184
|
+
logger.info(f" {green('fixed')} {check.name:15s} {check.fix()}")
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
logger.info(f" {yellow('!!')} {check.name:15s} fix failed: {exc}")
|
|
187
|
+
|
|
188
|
+
remaining = len(report.warnings) - len(fixable)
|
|
189
|
+
if remaining > 0:
|
|
190
|
+
logger.info("")
|
|
191
|
+
logger.info(f" {yellow(f'{remaining} warning(s) left')} {dim('— no automatic fix')}")
|
|
169
192
|
|
|
170
193
|
return 0
|
|
171
194
|
|
|
@@ -13,6 +13,7 @@ from collections.abc import Callable
|
|
|
13
13
|
from ixt.cli.cmd_apply import cmd_apply, cmd_export
|
|
14
14
|
from ixt.cli.cmd_cache import cmd_cache
|
|
15
15
|
from ixt.cli.cmd_config import cmd_config
|
|
16
|
+
from ixt.cli.cmd_exec import cmd_exec
|
|
16
17
|
from ixt.cli.cmd_info import cmd_info, cmd_list
|
|
17
18
|
from ixt.cli.cmd_install import _log_not_found_hint, cmd_add, cmd_install, cmd_uninstall
|
|
18
19
|
from ixt.cli.cmd_lock import cmd_lock
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"cmd_config",
|
|
37
38
|
"cmd_doctor",
|
|
38
39
|
"cmd_environment",
|
|
40
|
+
"cmd_exec",
|
|
39
41
|
"cmd_export",
|
|
40
42
|
"cmd_info",
|
|
41
43
|
"cmd_install",
|
|
@@ -58,6 +60,7 @@ COMMANDS: dict[str, Callable] = {
|
|
|
58
60
|
"upgrade": cmd_upgrade,
|
|
59
61
|
"list": cmd_list,
|
|
60
62
|
"info": cmd_info,
|
|
63
|
+
"exec": cmd_exec,
|
|
61
64
|
"config": cmd_config,
|
|
62
65
|
"shell": cmd_shell,
|
|
63
66
|
"where": cmd_where,
|
|
@@ -529,6 +529,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
529
529
|
action="store_true",
|
|
530
530
|
help="Skip GitHub, PyPI, npm, and latest-version network probes",
|
|
531
531
|
)
|
|
532
|
+
p_doctor.add_argument(
|
|
533
|
+
"--fix",
|
|
534
|
+
action="store_true",
|
|
535
|
+
help="Repair the findings that have an unambiguous remedy",
|
|
536
|
+
)
|
|
532
537
|
|
|
533
538
|
# -- environment -----------------------------------------------------------
|
|
534
539
|
p_environment = sub.add_parser("environment", help="Show ixt configuration and paths")
|
|
@@ -549,6 +554,38 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
549
554
|
p_where.set_defaults(command="where")
|
|
550
555
|
p_where.add_argument("tool", help="Tool spec or @id:<installed-id>")
|
|
551
556
|
|
|
557
|
+
# -- exec ------------------------------------------------------------------
|
|
558
|
+
# Declared for --help and shell completions only. The real parsing is
|
|
559
|
+
# short-circuited in cli.main, because everything after the tool name must
|
|
560
|
+
# reach the tool untouched, '--help' and '--' included.
|
|
561
|
+
p_exec = sub.add_parser(
|
|
562
|
+
"exec",
|
|
563
|
+
help="Run an exposed tool by name, without relying on PATH",
|
|
564
|
+
description=(
|
|
565
|
+
"Run an ixt-exposed tool by name. Useful under sudo, which replaces "
|
|
566
|
+
"PATH with secure_path and cannot see the ixt bin directory. "
|
|
567
|
+
"Every argument after the tool name is forwarded untouched."
|
|
568
|
+
),
|
|
569
|
+
epilog="Example: ixt exec --sudo rg -- PermitRootLogin /etc/ssh",
|
|
570
|
+
)
|
|
571
|
+
p_exec.set_defaults(command="exec")
|
|
572
|
+
p_exec.add_argument(
|
|
573
|
+
"--sudo",
|
|
574
|
+
action="store_true",
|
|
575
|
+
help="Run the tool through sudo (Unix only)",
|
|
576
|
+
)
|
|
577
|
+
p_exec.add_argument(
|
|
578
|
+
"--dry-run",
|
|
579
|
+
action="store_true",
|
|
580
|
+
help="Print the resolved command without running it",
|
|
581
|
+
)
|
|
582
|
+
p_exec.add_argument("tool", help="Exposed tool name")
|
|
583
|
+
p_exec.add_argument(
|
|
584
|
+
"args",
|
|
585
|
+
nargs=argparse.REMAINDER,
|
|
586
|
+
help="Arguments forwarded to the tool ('--' recommended before them)",
|
|
587
|
+
)
|
|
588
|
+
|
|
552
589
|
# -- setup -----------------------------------------------------------------
|
|
553
590
|
p_setup = sub.add_parser("setup", help="Configure shell integration (PATH, ...)")
|
|
554
591
|
p_setup.set_defaults(command="setup")
|