ixt-cli 0.9.0__tar.gz → 0.9.1__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.0 → ixt_cli-0.9.1}/PKG-INFO +31 -28
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/README.md +30 -27
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/pyproject.toml +4 -3
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/backends/binary.py +77 -9
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/backends/node.py +2 -4
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_config.py +69 -51
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_install.py +5 -2
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_lock.py +2 -2
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/asset_pattern_cache.py +2 -12
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/env_policy.py +7 -2
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/flags.py +3 -3
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/heuristics.py +86 -4
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/models.py +3 -12
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/apply.py +28 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/cache.py +45 -25
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/discover.py +119 -30
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/expose.py +106 -42
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/extract.py +84 -12
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/inject.py +51 -20
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/install.py +70 -80
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/install_local.py +51 -63
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/lockfile.py +214 -20
- ixt_cli-0.9.1/src/ixt/core/locks.py +112 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/pathlink.py +52 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/resolve_cache.py +4 -12
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/save.py +62 -25
- ixt_cli-0.9.1/src/ixt/core/transactions.py +216 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/upgrade.py +22 -106
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/data/heuristics.toml +8 -2
- ixt_cli-0.9.1/src/ixt/libs/atomic.py +56 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/gitlab_api.py +8 -8
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/http.py +86 -3
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/conftest.py +54 -20
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/README.md +6 -4
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/docker/Dockerfile.alpine +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/docker/Dockerfile.fedora +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/docker/Dockerfile.ubuntu +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/run_e2e_incus.sh +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/test_e2e.py +18 -8
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/integration/test_lifecycle.py +3 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_apply.py +37 -0
- ixt_cli-0.9.1/tests/unit/test_atomic.py +61 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_binary_backend.py +292 -40
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_cache.py +49 -2
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_cli.py +40 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_discover.py +112 -6
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_env_policy.py +18 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_export.py +1 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_expose.py +76 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_extract.py +60 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_gitlab_api.py +41 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_heuristics.py +53 -6
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_http.py +127 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_install.py +135 -1
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_install_local.py +30 -4
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_lock_write.py +308 -6
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_lockfile.py +1 -1
- ixt_cli-0.9.1/tests/unit/test_pathlink.py +145 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_save.py +46 -0
- ixt_cli-0.9.1/tests/unit/test_transactions.py +92 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_upgrade.py +33 -20
- ixt_cli-0.9.0/src/ixt/core/locks.py +0 -54
- ixt_cli-0.9.0/tests/unit/test_pathlink.py +0 -62
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/.gitignore +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/LICENSE +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/__main__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/backends/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/backends/binary_resolver.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/backends/python.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/argparse_completion.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_apply.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_cache.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_info.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_misc.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_registry.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/cmd_upgrade.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/commands.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/parser.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/cli/render.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/asset_index.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/fs_policy.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/lockfile.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/registry.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/settings.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/setup_toml.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/config/toml.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/apply_actions.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/bootstrap.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/doctor.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/export.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/hooks.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/identity.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/resolution_stats.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/resolve.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/runtimes.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/setup_completions.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/core/setup_path.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/data/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/data/asset_index.json +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/data/registry.toml +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/constants.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/fmt.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/logger.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/output.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/req_spec.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/semver.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/shell.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/libs/style.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/github_api.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/npm.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/pypi.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/net/source.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/platform/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/src/ixt/platform/win.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/conftest.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/playground_incus.sh +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/run_e2e_docker.sh +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/e2e/runtime_matrix.sh +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/integration/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/integration/test_policy_runtime.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_argparse_completion.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_asset_index.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_asset_pattern_cache.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_bootstrap.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_doctor.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_flags.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_fs_policy.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_github_api.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_hooks.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_inject.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_models.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_namespacing.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_node_backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_output.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_platform.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_python_backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_registry.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_req_spec.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_resolve.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_resolve_cache.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_runtimes.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_semver.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_settings.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_setup_path.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_setup_toml.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_source.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_style.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.9.1}/tests/unit/test_toml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ixt-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -51,10 +51,10 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
*Zero third-party deps. Zero system pollution. One command, any ecosystem.*
|
|
52
52
|
|
|
53
53
|
- **Day-one GitHub releases** — install a tool the day it ships, without
|
|
54
|
-
|
|
54
|
+
waiting for a formula, registry entry, or plugin.
|
|
55
55
|
- **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
bootstrapped on first use, so the host does not need Node, npm, or a package
|
|
57
|
+
manager pre-installed.
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
ixt tool install ruff # Python (PyPI)
|
|
@@ -202,40 +202,43 @@ ixt tool apply --locked --strict
|
|
|
202
202
|
|
|
203
203
|
`--locked` replays the exact lock instead of resolving newer versions.
|
|
204
204
|
`--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
|
|
205
|
-
diverge.
|
|
205
|
+
diverge. The strict preflight is local: it checks the manifest fingerprint,
|
|
206
|
+
declared policy, version constraints, and locked binary metadata without
|
|
207
|
+
contacting PyPI, npm, GitHub, or GitLab. Run `ixt lock write` when you want to
|
|
208
|
+
resolve upstream again and refresh the lock.
|
|
206
209
|
|
|
207
210
|
## 🧰 Commands
|
|
208
211
|
|
|
209
212
|
| Command | Description |
|
|
210
213
|
| ---------------------------------------------- | --------------------------------------------------------------------- |
|
|
211
|
-
| `ixt tool add <pkg>`
|
|
212
|
-
| `ixt tool install <pkg>`
|
|
213
|
-
| `ixt tool install <pkg> --bare`
|
|
214
|
-
| `ixt tool uninstall <pkg>`
|
|
215
|
-
| `ixt tool list`
|
|
216
|
-
| `ixt tool info <pkg>`
|
|
217
|
-
| `ixt tool upgrade <pkg>`
|
|
218
|
-
| `ixt tool upgrade --all`
|
|
219
|
-
| `ixt tool apply`
|
|
220
|
-
| `ixt tool apply --locked --strict`
|
|
221
|
-
| `ixt tool export`
|
|
222
|
-
| `ixt lock write`
|
|
223
|
-
| `ixt lock check`
|
|
224
|
-
| `ixt lock diff`
|
|
214
|
+
| `ixt tool add <pkg>` | Add a tool to ixt.toml without installing |
|
|
215
|
+
| `ixt tool install <pkg>` | Install a tool in isolation |
|
|
216
|
+
| `ixt tool install <pkg> --bare` | Install the env without exposing any binary |
|
|
217
|
+
| `ixt tool uninstall <pkg>` | Remove a tool |
|
|
218
|
+
| `ixt tool list` | List installed tools |
|
|
219
|
+
| `ixt tool info <pkg>` | Show tool details |
|
|
220
|
+
| `ixt tool upgrade <pkg>` | Upgrade a tool |
|
|
221
|
+
| `ixt tool upgrade --all` | Upgrade all tools |
|
|
222
|
+
| `ixt tool apply` | Sync tools from declarative config |
|
|
223
|
+
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
224
|
+
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
225
|
+
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
226
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
227
|
+
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
225
228
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
226
|
-
| `ixt tool config <pkg> expose <rules...>`
|
|
227
|
-
| `ixt tool config <pkg> inject <dep>`
|
|
228
|
-
| `ixt tool config <pkg> uninject <dep>`
|
|
229
|
-
| `ixt tool config __all__ expose <rules...>`
|
|
230
|
-
| `ixt tool shell <pkg>`
|
|
231
|
-
| `ixt tool where <pkg>`
|
|
229
|
+
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
230
|
+
| `ixt tool config <pkg> inject <dep>` | Inject a package into the tool's env |
|
|
231
|
+
| `ixt tool config <pkg> uninject <dep>` | Remove an injected package |
|
|
232
|
+
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
233
|
+
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
234
|
+
| `ixt tool where <pkg>` | Print the tool env path |
|
|
232
235
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
233
236
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
234
237
|
| `ixt cache info` | Show cache paths and sizes |
|
|
235
238
|
| `ixt cache prune` | Keep current + previous indexed download artifacts per repository |
|
|
236
239
|
| `ixt cache clear {downloads,metadata,tmp,all}` | Wipe cache(s); never touches installed tools |
|
|
237
|
-
| `ixt runtime info`
|
|
238
|
-
| `ixt runtime prune [--all]`
|
|
240
|
+
| `ixt runtime info` | Show ixt-managed runtime state |
|
|
241
|
+
| `ixt runtime prune [--all]` | Remove unused or all ixt-managed runtimes |
|
|
239
242
|
| `ixt setup path` | Add `$IXT_HOME/installed/bin` to PATH |
|
|
240
243
|
| `ixt setup completions --shell zsh` | Print shell completion code for ixt |
|
|
241
244
|
|
|
@@ -285,7 +288,7 @@ ixt is **not** a runtime version manager (mise, proto, asdf cover that). It mana
|
|
|
285
288
|
git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
|
|
286
289
|
uv sync --group dev
|
|
287
290
|
|
|
288
|
-
uv run pytest #
|
|
291
|
+
uv run pytest # hermetic unit suite
|
|
289
292
|
uv run ruff check src/ tests/ # lint
|
|
290
293
|
uv run ruff format src/ tests/ # format
|
|
291
294
|
```
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
*Zero third-party deps. Zero system pollution. One command, any ecosystem.*
|
|
23
23
|
|
|
24
24
|
- **Day-one GitHub releases** — install a tool the day it ships, without
|
|
25
|
-
|
|
25
|
+
waiting for a formula, registry entry, or plugin.
|
|
26
26
|
- **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
bootstrapped on first use, so the host does not need Node, npm, or a package
|
|
28
|
+
manager pre-installed.
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
ixt tool install ruff # Python (PyPI)
|
|
@@ -173,40 +173,43 @@ ixt tool apply --locked --strict
|
|
|
173
173
|
|
|
174
174
|
`--locked` replays the exact lock instead of resolving newer versions.
|
|
175
175
|
`--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
|
|
176
|
-
diverge.
|
|
176
|
+
diverge. The strict preflight is local: it checks the manifest fingerprint,
|
|
177
|
+
declared policy, version constraints, and locked binary metadata without
|
|
178
|
+
contacting PyPI, npm, GitHub, or GitLab. Run `ixt lock write` when you want to
|
|
179
|
+
resolve upstream again and refresh the lock.
|
|
177
180
|
|
|
178
181
|
## 🧰 Commands
|
|
179
182
|
|
|
180
183
|
| Command | Description |
|
|
181
184
|
| ---------------------------------------------- | --------------------------------------------------------------------- |
|
|
182
|
-
| `ixt tool add <pkg>`
|
|
183
|
-
| `ixt tool install <pkg>`
|
|
184
|
-
| `ixt tool install <pkg> --bare`
|
|
185
|
-
| `ixt tool uninstall <pkg>`
|
|
186
|
-
| `ixt tool list`
|
|
187
|
-
| `ixt tool info <pkg>`
|
|
188
|
-
| `ixt tool upgrade <pkg>`
|
|
189
|
-
| `ixt tool upgrade --all`
|
|
190
|
-
| `ixt tool apply`
|
|
191
|
-
| `ixt tool apply --locked --strict`
|
|
192
|
-
| `ixt tool export`
|
|
193
|
-
| `ixt lock write`
|
|
194
|
-
| `ixt lock check`
|
|
195
|
-
| `ixt lock diff`
|
|
185
|
+
| `ixt tool add <pkg>` | Add a tool to ixt.toml without installing |
|
|
186
|
+
| `ixt tool install <pkg>` | Install a tool in isolation |
|
|
187
|
+
| `ixt tool install <pkg> --bare` | Install the env without exposing any binary |
|
|
188
|
+
| `ixt tool uninstall <pkg>` | Remove a tool |
|
|
189
|
+
| `ixt tool list` | List installed tools |
|
|
190
|
+
| `ixt tool info <pkg>` | Show tool details |
|
|
191
|
+
| `ixt tool upgrade <pkg>` | Upgrade a tool |
|
|
192
|
+
| `ixt tool upgrade --all` | Upgrade all tools |
|
|
193
|
+
| `ixt tool apply` | Sync tools from declarative config |
|
|
194
|
+
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
195
|
+
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
196
|
+
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
197
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
198
|
+
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
196
199
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
197
|
-
| `ixt tool config <pkg> expose <rules...>`
|
|
198
|
-
| `ixt tool config <pkg> inject <dep>`
|
|
199
|
-
| `ixt tool config <pkg> uninject <dep>`
|
|
200
|
-
| `ixt tool config __all__ expose <rules...>`
|
|
201
|
-
| `ixt tool shell <pkg>`
|
|
202
|
-
| `ixt tool where <pkg>`
|
|
200
|
+
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
201
|
+
| `ixt tool config <pkg> inject <dep>` | Inject a package into the tool's env |
|
|
202
|
+
| `ixt tool config <pkg> uninject <dep>` | Remove an injected package |
|
|
203
|
+
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
204
|
+
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
205
|
+
| `ixt tool where <pkg>` | Print the tool env path |
|
|
203
206
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
204
207
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
205
208
|
| `ixt cache info` | Show cache paths and sizes |
|
|
206
209
|
| `ixt cache prune` | Keep current + previous indexed download artifacts per repository |
|
|
207
210
|
| `ixt cache clear {downloads,metadata,tmp,all}` | Wipe cache(s); never touches installed tools |
|
|
208
|
-
| `ixt runtime info`
|
|
209
|
-
| `ixt runtime prune [--all]`
|
|
211
|
+
| `ixt runtime info` | Show ixt-managed runtime state |
|
|
212
|
+
| `ixt runtime prune [--all]` | Remove unused or all ixt-managed runtimes |
|
|
210
213
|
| `ixt setup path` | Add `$IXT_HOME/installed/bin` to PATH |
|
|
211
214
|
| `ixt setup completions --shell zsh` | Print shell completion code for ixt |
|
|
212
215
|
|
|
@@ -256,7 +259,7 @@ ixt is **not** a runtime version manager (mise, proto, asdf cover that). It mana
|
|
|
256
259
|
git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
|
|
257
260
|
uv sync --group dev
|
|
258
261
|
|
|
259
|
-
uv run pytest #
|
|
262
|
+
uv run pytest # hermetic unit suite
|
|
260
263
|
uv run ruff check src/ tests/ # lint
|
|
261
264
|
uv run ruff format src/ tests/ # format
|
|
262
265
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ixt-cli"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.1" # 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"
|
|
@@ -61,14 +61,15 @@ include = [
|
|
|
61
61
|
# Testing
|
|
62
62
|
[tool.pytest.ini_options]
|
|
63
63
|
minversion = "7.0"
|
|
64
|
-
testpaths = ["tests"]
|
|
64
|
+
testpaths = ["tests/unit"]
|
|
65
65
|
python_files = ["test_*.py"]
|
|
66
66
|
python_classes = ["Test*"]
|
|
67
67
|
python_functions = ["test_*"]
|
|
68
|
-
addopts =
|
|
68
|
+
addopts = '-ra --strict-markers -m "not slow and not network"'
|
|
69
69
|
markers = [
|
|
70
70
|
"unit: unit tests",
|
|
71
71
|
"integration: integration tests",
|
|
72
|
+
"e2e: end-to-end tests against an installed ixt CLI",
|
|
72
73
|
"platform: platform-specific tests (linux, macos, windows)",
|
|
73
74
|
"slow: tests that bootstrap external runtimes (uv, bun) — skip by default with -m 'not slow'",
|
|
74
75
|
"network: tests that hit the public internet (e.g. real GitHub releases) — opt in with -m 'network'",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
7
8
|
from dataclasses import dataclass, field
|
|
@@ -18,6 +19,7 @@ from ixt.config.flags import is_setup_toml_enabled
|
|
|
18
19
|
from ixt.config.heuristics import (
|
|
19
20
|
get_name_suffix_tokens,
|
|
20
21
|
get_platform_aliases,
|
|
22
|
+
is_asset_arch_compatible,
|
|
21
23
|
load_heuristics,
|
|
22
24
|
select_asset,
|
|
23
25
|
)
|
|
@@ -30,6 +32,7 @@ from ixt.core.backend import BackendType
|
|
|
30
32
|
from ixt.core.discover import find_executables
|
|
31
33
|
from ixt.core.extract import extract, is_archive
|
|
32
34
|
from ixt.libs.logger import get_logger, get_verbosity
|
|
35
|
+
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError
|
|
33
36
|
from ixt.net.source import Asset, Release, ReleaseSource, RepoSpec, parse_spec
|
|
34
37
|
from ixt.platform import get_arch, get_os
|
|
35
38
|
|
|
@@ -45,6 +48,9 @@ __all__ = [
|
|
|
45
48
|
"make_source",
|
|
46
49
|
]
|
|
47
50
|
|
|
51
|
+
BINARY_ASSET_CACHE_SCHEMA = 2
|
|
52
|
+
_CACHE_ARCHIVE_SUFFIXES = (".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".txz", ".zip")
|
|
53
|
+
|
|
48
54
|
|
|
49
55
|
# 3.10 lacks typing.NotRequired (added in 3.11) and the project bans
|
|
50
56
|
# third-party deps (typing_extensions). The required-base + total=False
|
|
@@ -267,10 +273,51 @@ def _cache_dir_for(settings: Settings, repo_spec: RepoSpec) -> Path:
|
|
|
267
273
|
return settings.cache_dir / host / f"{repo_spec.owner}-{repo_spec.repo}"
|
|
268
274
|
|
|
269
275
|
|
|
270
|
-
def
|
|
271
|
-
"""Return the
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
def _asset_cache_key(repo_spec: RepoSpec, asset: Asset, *, tag: str) -> str:
|
|
277
|
+
"""Return the stable cache identity for one exact release asset."""
|
|
278
|
+
identity = json.dumps(
|
|
279
|
+
{
|
|
280
|
+
"schema": BINARY_ASSET_CACHE_SCHEMA,
|
|
281
|
+
"platform": repo_spec.platform,
|
|
282
|
+
"host": repo_spec.host,
|
|
283
|
+
"owner": repo_spec.owner,
|
|
284
|
+
"repo": repo_spec.repo,
|
|
285
|
+
"tag": tag,
|
|
286
|
+
"asset": asset.name,
|
|
287
|
+
"url": asset.url,
|
|
288
|
+
},
|
|
289
|
+
sort_keys=True,
|
|
290
|
+
separators=(",", ":"),
|
|
291
|
+
)
|
|
292
|
+
return hashlib.sha256(identity.encode()).hexdigest()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _asset_cache_path(
|
|
296
|
+
downloads_dir: Path,
|
|
297
|
+
repo_spec: RepoSpec,
|
|
298
|
+
asset: Asset,
|
|
299
|
+
*,
|
|
300
|
+
tag: str,
|
|
301
|
+
) -> Path:
|
|
302
|
+
"""Return a portable path for one exact release asset."""
|
|
303
|
+
lower_name = asset.name.lower()
|
|
304
|
+
suffix = next((item for item in _CACHE_ARCHIVE_SUFFIXES if lower_name.endswith(item)), "")
|
|
305
|
+
if not suffix:
|
|
306
|
+
match = re.search(r"\.[A-Za-z0-9]{1,10}$", asset.name)
|
|
307
|
+
suffix = match.group(0) if match is not None else ""
|
|
308
|
+
key = _asset_cache_key(repo_spec, asset, tag=tag)
|
|
309
|
+
return downloads_dir / f"assets-v{BINARY_ASSET_CACHE_SCHEMA}" / key / f"asset{suffix}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _cached_asset(path: Path, expected_size: int = 0) -> Path | None:
|
|
313
|
+
"""Return *path* when it is a non-empty cache entry of the expected size."""
|
|
314
|
+
try:
|
|
315
|
+
if not path.is_file():
|
|
316
|
+
return None
|
|
317
|
+
size = path.stat().st_size
|
|
318
|
+
except OSError:
|
|
319
|
+
return None
|
|
320
|
+
if 0 < size <= MAX_DOWNLOAD_SIZE and (expected_size <= 0 or size == expected_size):
|
|
274
321
|
return path
|
|
275
322
|
return None
|
|
276
323
|
|
|
@@ -536,6 +583,12 @@ class BinaryBackend:
|
|
|
536
583
|
asset = self._select_asset(release, selection_pattern, tool_name, spec)
|
|
537
584
|
else:
|
|
538
585
|
asset = release.assets[0]
|
|
586
|
+
if not is_asset_arch_compatible(
|
|
587
|
+
asset.name,
|
|
588
|
+
heuristics=load_heuristics(),
|
|
589
|
+
declared_platform=locked_binary.platform,
|
|
590
|
+
):
|
|
591
|
+
raise AssetSelectionError(spec, release.tag, [asset.name])
|
|
539
592
|
log.debug(f"selected asset: {asset.name}")
|
|
540
593
|
|
|
541
594
|
archive_path = self._download_asset(
|
|
@@ -950,15 +1003,28 @@ class BinaryBackend:
|
|
|
950
1003
|
upgrade: bool = False,
|
|
951
1004
|
) -> Path:
|
|
952
1005
|
"""Download an asset, using cache when available."""
|
|
953
|
-
|
|
1006
|
+
if asset.size > MAX_DOWNLOAD_SIZE:
|
|
1007
|
+
raise DownloadTooLargeError(asset.url, MAX_DOWNLOAD_SIZE, asset.size)
|
|
1008
|
+
|
|
1009
|
+
cache_key = _asset_cache_key(repo_spec, asset, tag=tag)
|
|
1010
|
+
dest = _asset_cache_path(self.settings.downloads_dir, repo_spec, asset, tag=tag)
|
|
1011
|
+
cached = _cached_asset(dest, asset.size)
|
|
954
1012
|
if cached is not None and not upgrade:
|
|
955
|
-
self._record_download(repo_spec, asset, cached, tag=tag)
|
|
1013
|
+
self._record_download(repo_spec, asset, cached, tag=tag, cache_key=cache_key)
|
|
956
1014
|
return cached
|
|
957
1015
|
|
|
958
|
-
|
|
959
|
-
dest
|
|
1016
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1017
|
+
dest.unlink(missing_ok=True)
|
|
960
1018
|
source.download_asset(asset.url, dest)
|
|
961
|
-
|
|
1019
|
+
if dest.is_file() and dest.stat().st_size > MAX_DOWNLOAD_SIZE:
|
|
1020
|
+
actual_size = dest.stat().st_size
|
|
1021
|
+
dest.unlink(missing_ok=True)
|
|
1022
|
+
raise DownloadTooLargeError(asset.url, MAX_DOWNLOAD_SIZE, actual_size)
|
|
1023
|
+
if _cached_asset(dest, asset.size) is None:
|
|
1024
|
+
dest.unlink(missing_ok=True)
|
|
1025
|
+
expected = f"{asset.size} bytes" if asset.size > 0 else "a non-empty file"
|
|
1026
|
+
raise RuntimeError(f"Downloaded asset {asset.name!r} does not match {expected}")
|
|
1027
|
+
self._record_download(repo_spec, asset, dest, tag=tag, cache_key=cache_key)
|
|
962
1028
|
return dest
|
|
963
1029
|
|
|
964
1030
|
def _record_download(
|
|
@@ -968,6 +1034,7 @@ class BinaryBackend:
|
|
|
968
1034
|
path: Path,
|
|
969
1035
|
*,
|
|
970
1036
|
tag: str,
|
|
1037
|
+
cache_key: str,
|
|
971
1038
|
) -> None:
|
|
972
1039
|
"""Index the downloaded binary asset for later cache pruning."""
|
|
973
1040
|
from ixt.core.cache import record_download
|
|
@@ -984,4 +1051,5 @@ class BinaryBackend:
|
|
|
984
1051
|
asset_url=asset.url,
|
|
985
1052
|
asset_size=asset.size,
|
|
986
1053
|
path=path,
|
|
1054
|
+
cache_key=cache_key,
|
|
987
1055
|
)
|
|
@@ -114,8 +114,7 @@ def _write_metadata(
|
|
|
114
114
|
"package_name": package_name,
|
|
115
115
|
"version": version,
|
|
116
116
|
"binaries": {
|
|
117
|
-
name: _encode_path_under_env(path, env_dir)
|
|
118
|
-
for name, path in binaries.items()
|
|
117
|
+
name: _encode_path_under_env(path, env_dir) for name, path in binaries.items()
|
|
119
118
|
},
|
|
120
119
|
"used_npm_fallback": used_npm_fallback,
|
|
121
120
|
"runtime": runtime,
|
|
@@ -136,8 +135,7 @@ def read_metadata(env_dir: Path) -> dict | None:
|
|
|
136
135
|
binaries = data.get("binaries")
|
|
137
136
|
if isinstance(binaries, dict):
|
|
138
137
|
data["binaries"] = {
|
|
139
|
-
name: _decode_path_from_env(path, env_dir)
|
|
140
|
-
for name, path in binaries.items()
|
|
138
|
+
name: _decode_path_from_env(path, env_dir) for name, path in binaries.items()
|
|
141
139
|
}
|
|
142
140
|
return data
|
|
143
141
|
|
|
@@ -18,6 +18,7 @@ _ALL_TOOLS_SENTINEL = "__all__"
|
|
|
18
18
|
def cmd_config(args: argparse.Namespace) -> int:
|
|
19
19
|
"""Dispatch ``ixt tool config <target> <action>`` to the right handler."""
|
|
20
20
|
logger = get_logger("config")
|
|
21
|
+
settings = None
|
|
21
22
|
if args.tool != _ALL_TOOLS_SENTINEL:
|
|
22
23
|
from ixt.config.settings import get_settings
|
|
23
24
|
from ixt.core.install import resolve_tool_name
|
|
@@ -28,7 +29,16 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
28
29
|
except ValueError as e:
|
|
29
30
|
logger.error(str(e))
|
|
30
31
|
return 1
|
|
31
|
-
|
|
32
|
+
try:
|
|
33
|
+
if settings is not None:
|
|
34
|
+
from ixt.core.locks import tool_lock
|
|
35
|
+
|
|
36
|
+
with tool_lock(args.tool, settings=settings):
|
|
37
|
+
return _CONFIG_ACTIONS[args.config_action](args, logger)
|
|
38
|
+
return _CONFIG_ACTIONS[args.config_action](args, logger)
|
|
39
|
+
except ValueError as e:
|
|
40
|
+
logger.error(str(e))
|
|
41
|
+
return 1
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
def _cmd_config_expose(args: argparse.Namespace, logger) -> int:
|
|
@@ -126,28 +136,32 @@ def _cmd_config_env(args: argparse.Namespace, logger) -> int:
|
|
|
126
136
|
return 0
|
|
127
137
|
|
|
128
138
|
from ixt.config.env_policy import apply_policy
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
record
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
139
|
+
from ixt.core.locks import bin_lock, tool_lock
|
|
140
|
+
|
|
141
|
+
with tool_lock(args.tool, settings=settings):
|
|
142
|
+
record = ToolRecord.load_json(meta_file)
|
|
143
|
+
if action == "reset":
|
|
144
|
+
record.env_base = "all"
|
|
145
|
+
record.env_allow = []
|
|
146
|
+
record.env_deny = {}
|
|
147
|
+
elif action == "base":
|
|
148
|
+
record.env_base = args.value
|
|
149
|
+
elif action == "allow":
|
|
150
|
+
for pat in args.patterns:
|
|
151
|
+
if pat not in record.env_allow:
|
|
152
|
+
record.env_allow.append(pat)
|
|
153
|
+
elif action == "deny":
|
|
154
|
+
exceptions = getattr(args, "exceptions", [])
|
|
155
|
+
for pat in args.patterns:
|
|
156
|
+
existing = record.env_deny.get(pat, {})
|
|
157
|
+
existing_except = existing.get("except", [])
|
|
158
|
+
record.env_deny[pat] = {"except": sorted(set(existing_except) | set(exceptions))}
|
|
159
|
+
|
|
160
|
+
record.save_json(meta_file)
|
|
161
|
+
|
|
162
|
+
if record.exposed_bins:
|
|
163
|
+
with bin_lock(settings=settings):
|
|
164
|
+
apply_policy(record, settings.bin_dir)
|
|
151
165
|
|
|
152
166
|
_warn_env_policy_without_bwrap(record, logger)
|
|
153
167
|
_print_env_policy(record, logger)
|
|
@@ -232,34 +246,38 @@ def _cmd_config_fs(args: argparse.Namespace, logger) -> int:
|
|
|
232
246
|
return 0
|
|
233
247
|
|
|
234
248
|
from ixt.config.env_policy import apply_policy
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
from ixt.core.locks import bin_lock, tool_lock
|
|
250
|
+
|
|
251
|
+
with tool_lock(args.tool, settings=settings):
|
|
252
|
+
record = ToolRecord.load_json(meta_file)
|
|
253
|
+
if action in ("ro", "rw", "scratch"):
|
|
254
|
+
_auto_promote_fs_base(record, logger)
|
|
255
|
+
|
|
256
|
+
if action == "reset":
|
|
257
|
+
record.fs_base = "all"
|
|
258
|
+
record.fs_ro = []
|
|
259
|
+
record.fs_rw = []
|
|
260
|
+
record.fs_scratch = []
|
|
261
|
+
elif action == "base":
|
|
262
|
+
record.fs_base = args.value
|
|
263
|
+
elif action == "ro":
|
|
264
|
+
for p in args.paths:
|
|
265
|
+
if p not in record.fs_ro:
|
|
266
|
+
record.fs_ro.append(p)
|
|
267
|
+
elif action == "rw":
|
|
268
|
+
for p in args.paths:
|
|
269
|
+
if p not in record.fs_rw:
|
|
270
|
+
record.fs_rw.append(p)
|
|
271
|
+
elif action == "scratch":
|
|
272
|
+
for p in args.paths:
|
|
273
|
+
if p not in record.fs_scratch:
|
|
274
|
+
record.fs_scratch.append(p)
|
|
275
|
+
|
|
276
|
+
record.save_json(meta_file)
|
|
277
|
+
|
|
278
|
+
if record.exposed_bins:
|
|
279
|
+
with bin_lock(settings=settings):
|
|
280
|
+
apply_policy(record, settings.bin_dir)
|
|
263
281
|
|
|
264
282
|
_print_fs_policy(record, logger)
|
|
265
283
|
return 0
|
|
@@ -238,14 +238,14 @@ def _resolve_runtime(choice: str | None) -> tuple[str | None, bool | None]:
|
|
|
238
238
|
|
|
239
239
|
|
|
240
240
|
def _check_setup_file_gate(args: argparse.Namespace, logger) -> int | None:
|
|
241
|
-
"""Guard
|
|
241
|
+
"""Guard the experimental author contract behind IXT_ENABLE_SETUP_TOML."""
|
|
242
242
|
if not getattr(args, "setup", None):
|
|
243
243
|
return None
|
|
244
244
|
from ixt.config.flags import SETUP_TOML_ENV, is_setup_toml_enabled
|
|
245
245
|
|
|
246
246
|
if is_setup_toml_enabled():
|
|
247
247
|
return None
|
|
248
|
-
logger.error("ixt.setup.toml support is disabled
|
|
248
|
+
logger.error("ixt.setup.toml support is disabled by default while experimental.")
|
|
249
249
|
logger.error(f" {dim(f'Set {SETUP_TOML_ENV}=1 to enable --setup-file and remote fetch.')}")
|
|
250
250
|
return 1
|
|
251
251
|
|
|
@@ -497,6 +497,9 @@ def _cmd_uninstall_one(args: argparse.Namespace, logger, settings, *, dry_run: b
|
|
|
497
497
|
logger.error(f"Tool '{args.target}' is not installed")
|
|
498
498
|
logger.error(f" {dim('Hint: run ixt tool list to see installed tools')}")
|
|
499
499
|
return 1
|
|
500
|
+
except ValueError as exc:
|
|
501
|
+
logger.error(str(exc))
|
|
502
|
+
return 1
|
|
500
503
|
|
|
501
504
|
logger.info(f" {red('-')} {bold(display)}")
|
|
502
505
|
|
|
@@ -51,7 +51,7 @@ def _cmd_lock_check() -> int:
|
|
|
51
51
|
return _log_lock_check_error(exc)
|
|
52
52
|
|
|
53
53
|
if result.ok:
|
|
54
|
-
logger.info(f"{result.path}
|
|
54
|
+
logger.info(f"{result.path} matches ixt.toml")
|
|
55
55
|
return 0
|
|
56
56
|
|
|
57
57
|
logger.error(f"{result.path} is out of date")
|
|
@@ -69,7 +69,7 @@ def _cmd_lock_diff() -> int:
|
|
|
69
69
|
return _log_lock_check_error(exc)
|
|
70
70
|
|
|
71
71
|
if result.ok:
|
|
72
|
-
data("ixt.lock
|
|
72
|
+
data("ixt.lock matches ixt.toml")
|
|
73
73
|
return 0
|
|
74
74
|
|
|
75
75
|
data("ixt.lock differs from ixt.toml:")
|
|
@@ -19,11 +19,10 @@ fresh release API call.
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import json
|
|
22
|
-
import os
|
|
23
|
-
import tempfile
|
|
24
22
|
from pathlib import Path
|
|
25
23
|
|
|
26
24
|
from ixt.config.settings import get_ixt_metadata_dir
|
|
25
|
+
from ixt.libs.atomic import atomic_write_text
|
|
27
26
|
|
|
28
27
|
CACHE_FILENAME = "asset_patterns.json"
|
|
29
28
|
|
|
@@ -52,16 +51,7 @@ def _load() -> dict[str, str]:
|
|
|
52
51
|
|
|
53
52
|
def _atomic_write(data: dict[str, str]) -> None:
|
|
54
53
|
path = _cache_path()
|
|
55
|
-
path.
|
|
56
|
-
fd, tmp = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=str(path.parent))
|
|
57
|
-
try:
|
|
58
|
-
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
59
|
-
json.dump(data, f, indent=2, sort_keys=True)
|
|
60
|
-
f.write("\n")
|
|
61
|
-
os.replace(tmp, path)
|
|
62
|
-
except Exception:
|
|
63
|
-
Path(tmp).unlink(missing_ok=True)
|
|
64
|
-
raise
|
|
54
|
+
atomic_write_text(path, json.dumps(data, indent=2, sort_keys=True) + "\n")
|
|
65
55
|
|
|
66
56
|
|
|
67
57
|
def get(owner: str, repo: str) -> str | None:
|
|
@@ -8,6 +8,8 @@ import stat
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
|
+
from ixt.core.pathlink import exposed_target_path
|
|
12
|
+
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from ixt.config.models import ToolRecord
|
|
13
15
|
|
|
@@ -322,8 +324,11 @@ def _needs_wrapper(record: ToolRecord) -> bool:
|
|
|
322
324
|
def apply_policy(record: ToolRecord, bin_dir: Path) -> None:
|
|
323
325
|
"""Replace symlinks with wrappers (or back) for all exposed bins of *record*."""
|
|
324
326
|
use_wrapper = _needs_wrapper(record)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
+
targets = [
|
|
328
|
+
(exposed_name, real_path, exposed_target_path(bin_dir, exposed_name))
|
|
329
|
+
for exposed_name, real_path in record.exposed_bins.items()
|
|
330
|
+
]
|
|
331
|
+
for exposed_name, real_path, target in targets:
|
|
327
332
|
if target.exists() or target.is_symlink():
|
|
328
333
|
target.unlink()
|
|
329
334
|
if use_wrapper:
|