ixt-cli 0.8.0__py3-none-any.whl

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 (84) hide show
  1. ixt/__init__.py +8 -0
  2. ixt/__main__.py +8 -0
  3. ixt/backends/__init__.py +1 -0
  4. ixt/backends/binary.py +935 -0
  5. ixt/backends/binary_resolver.py +307 -0
  6. ixt/backends/node.py +490 -0
  7. ixt/backends/python.py +234 -0
  8. ixt/cli/__init__.py +31 -0
  9. ixt/cli/argparse_completion.py +557 -0
  10. ixt/cli/cmd_apply.py +404 -0
  11. ixt/cli/cmd_cache.py +86 -0
  12. ixt/cli/cmd_config.py +295 -0
  13. ixt/cli/cmd_info.py +116 -0
  14. ixt/cli/cmd_install.py +508 -0
  15. ixt/cli/cmd_misc.py +261 -0
  16. ixt/cli/cmd_registry.py +35 -0
  17. ixt/cli/cmd_upgrade.py +336 -0
  18. ixt/cli/commands.py +70 -0
  19. ixt/cli/parser.py +555 -0
  20. ixt/cli/render.py +85 -0
  21. ixt/config/__init__.py +5 -0
  22. ixt/config/asset_index.py +305 -0
  23. ixt/config/asset_pattern_cache.py +87 -0
  24. ixt/config/env_policy.py +340 -0
  25. ixt/config/flags.py +29 -0
  26. ixt/config/fs_policy.py +17 -0
  27. ixt/config/heuristics.py +465 -0
  28. ixt/config/models.py +176 -0
  29. ixt/config/registry.py +145 -0
  30. ixt/config/settings.py +173 -0
  31. ixt/config/setup_toml.py +179 -0
  32. ixt/config/toml.py +416 -0
  33. ixt/core/__init__.py +16 -0
  34. ixt/core/apply.py +564 -0
  35. ixt/core/apply_actions.py +106 -0
  36. ixt/core/backend.py +187 -0
  37. ixt/core/bootstrap.py +410 -0
  38. ixt/core/cache.py +332 -0
  39. ixt/core/discover.py +150 -0
  40. ixt/core/doctor.py +591 -0
  41. ixt/core/export.py +419 -0
  42. ixt/core/expose.py +350 -0
  43. ixt/core/extract.py +261 -0
  44. ixt/core/hooks.py +182 -0
  45. ixt/core/identity.py +148 -0
  46. ixt/core/inject.py +143 -0
  47. ixt/core/install.py +509 -0
  48. ixt/core/install_local.py +229 -0
  49. ixt/core/locks.py +54 -0
  50. ixt/core/pathlink.py +86 -0
  51. ixt/core/resolution_stats.py +191 -0
  52. ixt/core/resolve.py +150 -0
  53. ixt/core/resolve_cache.py +185 -0
  54. ixt/core/runtimes.py +192 -0
  55. ixt/core/save.py +237 -0
  56. ixt/core/setup_completions.py +11 -0
  57. ixt/core/setup_path.py +368 -0
  58. ixt/core/upgrade.py +596 -0
  59. ixt/data/__init__.py +10 -0
  60. ixt/data/asset_index.json +574 -0
  61. ixt/data/heuristics.toml +98 -0
  62. ixt/data/registry.toml +71 -0
  63. ixt/libs/__init__.py +3 -0
  64. ixt/libs/constants.py +4 -0
  65. ixt/libs/fmt.py +108 -0
  66. ixt/libs/logger.py +109 -0
  67. ixt/libs/output.py +25 -0
  68. ixt/libs/req_spec.py +115 -0
  69. ixt/libs/semver.py +149 -0
  70. ixt/libs/shell.py +126 -0
  71. ixt/libs/style.py +238 -0
  72. ixt/net/__init__.py +1 -0
  73. ixt/net/github_api.py +158 -0
  74. ixt/net/gitlab_api.py +149 -0
  75. ixt/net/http.py +194 -0
  76. ixt/net/npm.py +24 -0
  77. ixt/net/pypi.py +26 -0
  78. ixt/net/source.py +163 -0
  79. ixt/platform/__init__.py +131 -0
  80. ixt/platform/win.py +68 -0
  81. ixt_cli-0.8.0.dist-info/METADATA +294 -0
  82. ixt_cli-0.8.0.dist-info/RECORD +84 -0
  83. ixt_cli-0.8.0.dist-info/WHEEL +4 -0
  84. ixt_cli-0.8.0.dist-info/entry_points.txt +2 -0
ixt/backends/python.py ADDED
@@ -0,0 +1,234 @@
1
+ """Python backend — isolated venvs via uv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import configparser
6
+ import json
7
+ import os
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+
11
+ from ixt.config.settings import Settings, get_settings
12
+ from ixt.core.backend import BackendType
13
+ from ixt.libs.shell import shell_run
14
+ from ixt.platform import get_binary_extension, is_windows
15
+
16
+
17
+ class BinaryNotFoundError(FileNotFoundError):
18
+ """Raised when a binary is missing from the tool environment."""
19
+
20
+ def __init__(self, binary_name: str, env_dir: Path, available: list[str]):
21
+ self.binary_name = binary_name
22
+ self.env_dir = env_dir
23
+ self.available = available
24
+ avail = ", ".join(available) or "<none>"
25
+ super().__init__(f"Binary '{binary_name}' not found in {env_dir}. Available: {avail}")
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Filesystem helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def _iter_executables(directory: Path) -> list[Path]:
34
+ """List executable files in *directory*."""
35
+ if not directory.exists():
36
+ return []
37
+ if is_windows():
38
+ allowed = {".exe", ".bat", ".cmd", ".ps1"}
39
+ return sorted(p for p in directory.iterdir() if p.is_file() and p.suffix.lower() in allowed)
40
+ return sorted(p for p in directory.iterdir() if p.is_file() and os.access(p, os.X_OK))
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Console-scripts extraction (Python-specific metadata)
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ def _find_site_packages(venv_dir: Path) -> Path | None:
49
+ """Locate site-packages inside a ``.venv``."""
50
+ candidates = [venv_dir / "Lib" / "site-packages"]
51
+ candidates.extend(sorted((venv_dir / "lib").glob("python*/site-packages")))
52
+ for c in candidates:
53
+ if c.exists():
54
+ return c
55
+ return None
56
+
57
+
58
+ def _dist_info_package_name(dist_info_dir: Path) -> str:
59
+ metadata = dist_info_dir / "METADATA"
60
+ if not metadata.exists():
61
+ return ""
62
+ with metadata.open(encoding="utf-8") as f:
63
+ for line in f:
64
+ if line.startswith("Name:"):
65
+ return line.split(":", 1)[1].strip()
66
+ return ""
67
+
68
+
69
+ def _find_console_scripts(
70
+ site_packages: Path,
71
+ ) -> dict[str, dict[str, str]]:
72
+ """Map package names to their console_scripts entry points."""
73
+ scripts: dict[str, dict[str, str]] = {}
74
+ for dist in sorted(site_packages.glob("*.dist-info")):
75
+ pkg = _dist_info_package_name(dist)
76
+ if not pkg:
77
+ continue
78
+ ep = dist / "entry_points.txt"
79
+ if not ep.exists():
80
+ continue
81
+ parser = configparser.ConfigParser()
82
+ parser.read(ep, encoding="utf-8")
83
+ if not parser.has_section("console_scripts"):
84
+ continue
85
+ scripts[pkg] = dict(parser.items("console_scripts"))
86
+ return scripts
87
+
88
+
89
+ def _build_pip_metadata(
90
+ venv_dir: Path,
91
+ ) -> dict[str, dict[str, dict[str, str]]]:
92
+ site = _find_site_packages(venv_dir)
93
+ if site is None:
94
+ return {"console_scripts": {}}
95
+ return {"console_scripts": _find_console_scripts(site)}
96
+
97
+
98
+ def _write_pip_metadata(env_dir: Path, venv_dir: Path) -> Path:
99
+ path = env_dir / "pip_metadata.json"
100
+ path.parent.mkdir(parents=True, exist_ok=True)
101
+ with path.open("w", encoding="utf-8") as f:
102
+ json.dump(_build_pip_metadata(venv_dir), f, indent=2)
103
+ f.write("\n")
104
+ return path
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # PythonBackend
109
+ # ---------------------------------------------------------------------------
110
+
111
+
112
+ @dataclass
113
+ class PythonBackend:
114
+ """Install Python CLI tools in isolated venvs via ``uv``."""
115
+
116
+ settings: Settings = field(default_factory=get_settings)
117
+ backend_type: BackendType = field(default=BackendType.PYTHON, init=False)
118
+
119
+ # -- path helpers -------------------------------------------------------
120
+
121
+ def _venv_dir(self, env_dir: Path) -> Path:
122
+ return env_dir / ".venv"
123
+
124
+ def _bin_dir(self, env_dir: Path) -> Path:
125
+ return self._venv_dir(env_dir) / ("Scripts" if is_windows() else "bin")
126
+
127
+ def _python(self, env_dir: Path) -> Path:
128
+ name = "python.exe" if is_windows() else "python"
129
+ return self._bin_dir(env_dir) / name
130
+
131
+ def _uv(self) -> str:
132
+ from ixt.core.bootstrap import ensure_uv
133
+
134
+ return ensure_uv(self.settings)
135
+
136
+ def _run_uv_pip(self, env_dir: Path, command: str, *args: str) -> str:
137
+ result = shell_run(
138
+ [
139
+ self._uv(),
140
+ "pip",
141
+ command,
142
+ "--python",
143
+ str(self._python(env_dir)),
144
+ *args,
145
+ ],
146
+ capture_output=True,
147
+ check=True,
148
+ )
149
+ return result.stdout.strip() if result.stdout else ""
150
+
151
+ # -- Backend protocol ---------------------------------------------------
152
+
153
+ def env_exists(self, env_dir: Path) -> bool:
154
+ return self._venv_dir(env_dir).exists()
155
+
156
+ def create_env(self, env_dir: Path) -> bool:
157
+ env_dir.mkdir(parents=True, exist_ok=True)
158
+ if self.env_exists(env_dir):
159
+ return False
160
+ shell_run(
161
+ [self._uv(), "venv", str(self._venv_dir(env_dir))],
162
+ capture_output=True,
163
+ check=True,
164
+ )
165
+ return True
166
+
167
+ def install_packages(
168
+ self,
169
+ env_dir: Path,
170
+ specs: list[str],
171
+ *,
172
+ upgrade: bool = False,
173
+ ) -> None:
174
+ args: list[str] = []
175
+ if upgrade:
176
+ args.append("--upgrade")
177
+ args.extend(specs)
178
+ self._run_uv_pip(env_dir, "install", *args)
179
+ _write_pip_metadata(env_dir, self._venv_dir(env_dir))
180
+
181
+ def uninstall_package(self, env_dir: Path, package: str) -> None:
182
+ self._run_uv_pip(env_dir, "uninstall", package)
183
+
184
+ def find_binaries(self, env_dir: Path) -> list[Path]:
185
+ return _iter_executables(self._bin_dir(env_dir))
186
+
187
+ def get_package_binaries(self, env_dir: Path, package_name: str) -> dict[str, str]:
188
+ meta_file = env_dir / "pip_metadata.json"
189
+ if meta_file.exists():
190
+ with meta_file.open(encoding="utf-8") as f:
191
+ data = json.load(f)
192
+ return data.get("console_scripts", {}).get(package_name, {})
193
+ # Fallback: live scan when no cached metadata exists.
194
+ site = _find_site_packages(self._venv_dir(env_dir))
195
+ if site is None:
196
+ return {}
197
+ return _find_console_scripts(site).get(package_name, {})
198
+
199
+ def installed_version(self, env_dir: Path, package_name: str) -> str | None:
200
+ # Try reading version from dist-info METADATA (avoids subprocess).
201
+ site = _find_site_packages(self._venv_dir(env_dir))
202
+ if site is not None:
203
+ normalized = package_name.replace("-", "_").lower()
204
+ for dist in site.glob("*.dist-info"):
205
+ if not dist.name.lower().startswith(normalized + "-"):
206
+ continue
207
+ metadata = dist / "METADATA"
208
+ if not metadata.exists():
209
+ continue
210
+ with metadata.open(encoding="utf-8") as f:
211
+ for line in f:
212
+ if line.startswith("Version:"):
213
+ return line.split(":", 1)[1].strip()
214
+ # Fallback: parse `uv pip freeze` output.
215
+ for line in self.export(env_dir):
216
+ if "==" in line:
217
+ name, version = line.split("==", 1)
218
+ if name.strip().lower() == package_name.lower():
219
+ return version.strip()
220
+ return None
221
+
222
+ def export(self, env_dir: Path) -> list[str]:
223
+ output = self._run_uv_pip(env_dir, "freeze")
224
+ return [line for line in output.splitlines() if line]
225
+
226
+ # -- extra helpers (Python-specific) ------------------------------------
227
+
228
+ def venv_bin(self, env_dir: Path, name: str, *, fail_if_missing: bool = True) -> Path:
229
+ """Get a specific binary by name from the venv."""
230
+ path = self._bin_dir(env_dir) / f"{name}{get_binary_extension()}"
231
+ if fail_if_missing and not path.exists():
232
+ available = [p.stem for p in self.find_binaries(env_dir)]
233
+ raise BinaryNotFoundError(name, env_dir, available)
234
+ return path
ixt/cli/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """CLI module — entry point for the ixt command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["main"]
6
+
7
+
8
+ def main() -> int:
9
+ """Entry point for ixt CLI."""
10
+ from ixt.cli.commands import COMMANDS
11
+ from ixt.cli.parser import build_parser, normalize_verbosity
12
+
13
+ parser = build_parser()
14
+ args = parser.parse_args()
15
+
16
+ vlevel = normalize_verbosity(args)
17
+ if vlevel:
18
+ from ixt.libs.logger import Level, set_global_level
19
+
20
+ set_global_level(Level.DEBUG, verbosity=vlevel)
21
+
22
+ if not getattr(args, "command", None):
23
+ parser.print_help()
24
+ return 0
25
+
26
+ handler = COMMANDS.get(args.command)
27
+ if handler is None:
28
+ parser.print_help()
29
+ return 1
30
+
31
+ return handler(args)