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/config/toml.py ADDED
@@ -0,0 +1,416 @@
1
+ """TOML configuration parsing for ixt.toml files.
2
+
3
+ Uses stdlib tomllib (Python 3.11+) with a minimal fallback parser
4
+ for Python 3.10 that handles the subset of TOML used by ixt.toml.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sys
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from ixt.libs.constants import EXPOSE_MAIN
16
+
17
+ # -- TOML loading ------------------------------------------------------------
18
+
19
+ if sys.version_info >= (3, 11):
20
+ import tomllib
21
+
22
+ def _load_toml(path: Path) -> dict[str, Any]:
23
+ with path.open("rb") as f:
24
+ return tomllib.load(f)
25
+
26
+ else:
27
+
28
+ def _load_toml(path: Path) -> dict[str, Any]:
29
+ """Minimal TOML parser for ixt.toml (Python 3.10 fallback).
30
+
31
+ Handles the subset we use: tables, dotted keys, strings,
32
+ inline arrays, inline tables, booleans, and integers.
33
+ """
34
+ return _mini_toml_parse(path.read_text(encoding="utf-8"))
35
+
36
+
37
+ def _mini_toml_parse(text: str) -> dict[str, Any]:
38
+ """Parse a minimal TOML subset sufficient for ixt.toml."""
39
+ root: dict[str, Any] = {}
40
+ current = root
41
+ current_path: list[str] = []
42
+
43
+ for line in text.splitlines():
44
+ stripped = line.strip()
45
+ if not stripped or stripped.startswith("#"):
46
+ continue
47
+
48
+ # Table header: [section] or [section.subsection]
49
+ m = re.match(r"^\[([^\]]+)\]$", stripped)
50
+ if m:
51
+ current_path = [k.strip().strip('"') for k in m.group(1).split(".")]
52
+ current = root
53
+ for key in current_path:
54
+ current = current.setdefault(key, {})
55
+ continue
56
+
57
+ # Key = value
58
+ eq_pos = stripped.find("=")
59
+ if eq_pos < 0:
60
+ continue
61
+ key = stripped[:eq_pos].strip().strip('"')
62
+ val_str = stripped[eq_pos + 1 :].strip()
63
+ # Strip inline comment (not inside strings)
64
+ val_str = _strip_inline_comment(val_str)
65
+ current[key] = _parse_value(val_str)
66
+
67
+ return root
68
+
69
+
70
+ def _strip_inline_comment(val: str) -> str:
71
+ """Remove trailing # comment, respecting quoted strings."""
72
+ in_str = False
73
+ quote_char = ""
74
+ i = 0
75
+ while i < len(val):
76
+ c = val[i]
77
+ if in_str:
78
+ if c == "\\" and i + 1 < len(val):
79
+ i += 2
80
+ continue
81
+ if c == quote_char:
82
+ in_str = False
83
+ else:
84
+ if c in ('"', "'"):
85
+ in_str = True
86
+ quote_char = c
87
+ elif c == "#":
88
+ return val[:i].rstrip()
89
+ i += 1
90
+ return val
91
+
92
+
93
+ def _parse_value(val: str) -> Any:
94
+ """Parse a TOML value (string, bool, int, array, inline table)."""
95
+ if not val:
96
+ return ""
97
+
98
+ # Boolean
99
+ if val == "true":
100
+ return True
101
+ if val == "false":
102
+ return False
103
+
104
+ # Integer
105
+ if re.match(r"^-?\d+$", val):
106
+ return int(val)
107
+
108
+ # Quoted string
109
+ if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
110
+ return val[1:-1]
111
+
112
+ # Inline array: [...]
113
+ if val.startswith("[") and val.endswith("]"):
114
+ inner = val[1:-1].strip()
115
+ if not inner:
116
+ return []
117
+ return [_parse_value(item.strip()) for item in _split_top_level(inner, ",")]
118
+
119
+ # Inline table: {...}
120
+ if val.startswith("{") and val.endswith("}"):
121
+ inner = val[1:-1].strip()
122
+ if not inner:
123
+ return {}
124
+ result = {}
125
+ for item in _split_top_level(inner, ","):
126
+ eq = item.find("=")
127
+ if eq < 0:
128
+ continue
129
+ k = item[:eq].strip().strip('"')
130
+ v = item[eq + 1 :].strip()
131
+ result[k] = _parse_value(v)
132
+ return result
133
+
134
+ # Bare string (shouldn't happen in valid TOML, but handle gracefully)
135
+ return val
136
+
137
+
138
+ def _split_top_level(text: str, sep: str) -> list[str]:
139
+ """Split *text* by *sep* respecting brackets and quotes."""
140
+ parts: list[str] = []
141
+ depth = 0
142
+ in_str = False
143
+ quote_char = ""
144
+ start = 0
145
+ for i, c in enumerate(text):
146
+ if in_str:
147
+ if c == "\\" and i + 1 < len(text):
148
+ continue
149
+ if c == quote_char:
150
+ in_str = False
151
+ else:
152
+ if c in ('"', "'"):
153
+ in_str = True
154
+ quote_char = c
155
+ elif c in ("[", "{"):
156
+ depth += 1
157
+ elif c in ("]", "}"):
158
+ depth -= 1
159
+ elif c == sep and depth == 0:
160
+ parts.append(text[start:i])
161
+ start = i + 1
162
+ parts.append(text[start:])
163
+ return [p for p in parts if p.strip()]
164
+
165
+
166
+ # -- ixt.toml data model -----------------------------------------------------
167
+
168
+
169
+ @dataclass(slots=True)
170
+ class ToolSpec:
171
+ """Parsed tool specification from ixt.toml [tools] section."""
172
+
173
+ name: str # [tools] key: install spec by default, slot when ``install`` is set
174
+ install: str | None = None # underlying install spec for slotted entries
175
+ version: str | None = None
176
+ expose: list[str] = field(default_factory=lambda: [EXPOSE_MAIN])
177
+ inject: list[str] = field(default_factory=list)
178
+ node_shim: bool | None = None # None = auto (default), False = opt-out of #!node → bun rewrite
179
+ runtime: str | None = None # npm only — None/bun default, "node" = install via npm
180
+ asset_pattern: str | None = None # binary backend only — forced pattern for asset selection
181
+ env_base: str = "all"
182
+ env_allow: list[str] = field(default_factory=list)
183
+ env_deny: dict[str, dict[str, list[str]]] = field(default_factory=dict)
184
+ fs_base: str = "all"
185
+ fs_ro: list[str] = field(default_factory=list)
186
+ fs_rw: list[str] = field(default_factory=list)
187
+ fs_scratch: list[str] = field(default_factory=list)
188
+
189
+
190
+ @dataclass(slots=True)
191
+ class IxtConfig:
192
+ """Parsed ixt.toml configuration."""
193
+
194
+ tools: dict[str, ToolSpec] = field(default_factory=dict)
195
+ settings: dict[str, str] = field(default_factory=dict)
196
+ source_path: Path | None = None
197
+
198
+
199
+ # -- Parsing ixt.toml → IxtConfig -------------------------------------------
200
+
201
+
202
+ def parse_tool_entry(name: str, raw: dict[str, Any] | Any) -> ToolSpec:
203
+ """Parse a single tool entry from the [tools] table."""
204
+ if not isinstance(raw, dict):
205
+ raw = {}
206
+
207
+ legacy_fields = [field for field in ("pkg", "source") if field in raw]
208
+ if legacy_fields:
209
+ fields = ", ".join(legacy_fields)
210
+ raise ValueError(
211
+ f"Unsupported ixt.toml field(s) for tool '{name}': {fields}. "
212
+ 'Use install = "..." for slotted entries or @pypi:/@npm:/@gh:/@gl: prefixes.'
213
+ )
214
+
215
+ expose_raw = raw.get("expose")
216
+ if isinstance(expose_raw, str):
217
+ expose = [expose_raw]
218
+ elif isinstance(expose_raw, list):
219
+ expose = [str(e) for e in expose_raw]
220
+ else:
221
+ expose = [EXPOSE_MAIN]
222
+
223
+ inject_raw = raw.get("inject")
224
+ if isinstance(inject_raw, str):
225
+ inject = [inject_raw]
226
+ elif isinstance(inject_raw, list):
227
+ inject = [str(e) for e in inject_raw]
228
+ else:
229
+ inject = []
230
+
231
+ node_shim_raw = raw.get("node_shim")
232
+ node_shim = bool(node_shim_raw) if isinstance(node_shim_raw, bool) else None
233
+
234
+ runtime_raw = raw.get("runtime")
235
+ runtime = str(runtime_raw) if isinstance(runtime_raw, str) and runtime_raw else None
236
+
237
+ install_raw = raw.get("install")
238
+ install = str(install_raw) if isinstance(install_raw, str) and install_raw else None
239
+
240
+ asset_pattern_raw = raw.get("asset_pattern")
241
+ asset_pattern = (
242
+ str(asset_pattern_raw) if isinstance(asset_pattern_raw, str) and asset_pattern_raw else None
243
+ )
244
+
245
+ def _strlist(key: str) -> list[str]:
246
+ v = raw.get(key, [])
247
+ return [str(x) for x in v] if isinstance(v, list) else []
248
+
249
+ def _parse_deny(raw_deny: object) -> dict[str, dict[str, list[str]]]:
250
+ if not isinstance(raw_deny, dict):
251
+ return {}
252
+ result = {}
253
+ for pat, rule in raw_deny.items():
254
+ if isinstance(rule, dict):
255
+ excs = rule.get("except", [])
256
+ excs_list = [str(e) for e in excs] if isinstance(excs, list) else []
257
+ result[str(pat)] = {"except": excs_list}
258
+ else:
259
+ result[str(pat)] = {"except": []}
260
+ return result
261
+
262
+ return ToolSpec(
263
+ name=name,
264
+ install=install,
265
+ version=raw.get("version"),
266
+ expose=expose,
267
+ inject=inject,
268
+ node_shim=node_shim,
269
+ runtime=runtime,
270
+ asset_pattern=asset_pattern,
271
+ env_base=str(raw.get("env_base", "all")),
272
+ env_allow=_strlist("env_allow"),
273
+ env_deny=_parse_deny(raw.get("env_deny", {})),
274
+ fs_base="all" if (_raw_fs := raw.get("fs_base", "all")) == "none" else str(_raw_fs),
275
+ fs_ro=_strlist("fs_ro"),
276
+ fs_rw=_strlist("fs_rw"),
277
+ fs_scratch=_strlist("fs_scratch"),
278
+ )
279
+
280
+
281
+ def parse_config(data: dict[str, Any], source_path: Path | None = None) -> IxtConfig:
282
+ """Parse raw TOML dict into an IxtConfig."""
283
+ tools: dict[str, ToolSpec] = {}
284
+ for name, raw in data.get("tools", {}).items():
285
+ tools[name] = parse_tool_entry(name, raw)
286
+
287
+ settings = {}
288
+ for k, v in data.get("settings", {}).items():
289
+ settings[k] = str(v)
290
+
291
+ return IxtConfig(tools=tools, settings=settings, source_path=source_path)
292
+
293
+
294
+ def load_config(path: Path) -> IxtConfig:
295
+ """Load and parse an ixt.toml file."""
296
+ data = _load_toml(path)
297
+ return parse_config(data, source_path=path)
298
+
299
+
300
+ # -- File resolution ----------------------------------------------------------
301
+
302
+
303
+ def find_config_file(start: Path | None = None) -> Path | None:
304
+ """Find the nearest ixt.toml, searching upward then falling back to global.
305
+
306
+ Resolution order:
307
+ 1. ./ixt.toml (then parent dirs up to root)
308
+ 2. $IXT_HOME/config/ixt.toml
309
+ """
310
+ from ixt.config.settings import get_ixt_config_dir
311
+
312
+ cwd = (start or Path.cwd()).resolve()
313
+
314
+ # Walk up from cwd looking for ixt.toml
315
+ current = cwd
316
+ while True:
317
+ candidate = current / "ixt.toml"
318
+ if candidate.is_file():
319
+ return candidate
320
+ parent = current.parent
321
+ if parent == current:
322
+ break
323
+ current = parent
324
+
325
+ # Fall back to global config
326
+ global_config = get_ixt_config_dir() / "ixt.toml"
327
+ if global_config.is_file():
328
+ return global_config
329
+
330
+ return None
331
+
332
+
333
+ # -- TOML serialization (for export/save) ------------------------------------
334
+
335
+
336
+ def _quote_key(key: str) -> str:
337
+ """Quote a TOML key if it contains special characters."""
338
+ if re.match(r"^[A-Za-z0-9_-]+$", key):
339
+ return key
340
+ return f'"{key}"'
341
+
342
+
343
+ def _format_value(val: Any) -> str:
344
+ """Format a Python value as a TOML value string."""
345
+ if isinstance(val, bool):
346
+ return "true" if val else "false"
347
+ if isinstance(val, int):
348
+ return str(val)
349
+ if isinstance(val, str):
350
+ return f'"{val}"'
351
+ if isinstance(val, list):
352
+ items = ", ".join(_format_value(v) for v in val)
353
+ return f"[{items}]"
354
+ if isinstance(val, dict):
355
+ pairs = ", ".join(f"{_quote_key(k)} = {_format_value(v)}" for k, v in val.items())
356
+ return f"{{{pairs}}}"
357
+ return f'"{val}"'
358
+
359
+
360
+ def tool_spec_to_dict(spec: ToolSpec) -> dict[str, Any]:
361
+ """Convert a ToolSpec to its TOML-serializable dict form."""
362
+ d: dict[str, Any] = {}
363
+ if spec.install:
364
+ d["install"] = spec.install
365
+ if spec.version:
366
+ d["version"] = spec.version
367
+ if spec.expose != [EXPOSE_MAIN]:
368
+ d["expose"] = spec.expose
369
+ if spec.inject:
370
+ d["inject"] = spec.inject
371
+ if spec.node_shim is False:
372
+ d["node_shim"] = False
373
+ if spec.runtime:
374
+ d["runtime"] = spec.runtime
375
+ if spec.asset_pattern:
376
+ d["asset_pattern"] = spec.asset_pattern
377
+ if spec.env_base != "all":
378
+ d["env_base"] = spec.env_base
379
+ if spec.env_allow:
380
+ d["env_allow"] = spec.env_allow
381
+ if spec.env_deny:
382
+ d["env_deny"] = spec.env_deny
383
+ if spec.fs_base != "all":
384
+ d["fs_base"] = spec.fs_base
385
+ if spec.fs_ro:
386
+ d["fs_ro"] = spec.fs_ro
387
+ if spec.fs_rw:
388
+ d["fs_rw"] = spec.fs_rw
389
+ if spec.fs_scratch:
390
+ d["fs_scratch"] = spec.fs_scratch
391
+ return d
392
+
393
+
394
+ def serialize_config(config: IxtConfig) -> str:
395
+ """Serialize an IxtConfig to TOML string."""
396
+ lines: list[str] = []
397
+
398
+ if config.settings:
399
+ lines.append("[settings]")
400
+ for key, val in sorted(config.settings.items()):
401
+ lines.append(f'{_quote_key(key)} = "{val}"')
402
+ lines.append("")
403
+
404
+ if config.tools:
405
+ lines.append("[tools]")
406
+ for name in sorted(config.tools):
407
+ spec = config.tools[name]
408
+ d = tool_spec_to_dict(spec)
409
+ key = _quote_key(name)
410
+ if d:
411
+ lines.append(f"{key} = {_format_value(d)}")
412
+ else:
413
+ lines.append(f"{key} = {{}}")
414
+ lines.append("")
415
+
416
+ return "\n".join(lines) + "\n" if lines else ""
ixt/core/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """Core primitives for isolated tool environments."""
2
+
3
+ from ixt.core.backend import Backend, BackendType, detect_backend, get_backend
4
+ from ixt.core.install import install_tool, uninstall_tool
5
+ from ixt.core.pathlink import PathLink, create_path_link
6
+
7
+ __all__ = [
8
+ "Backend",
9
+ "BackendType",
10
+ "PathLink",
11
+ "create_path_link",
12
+ "detect_backend",
13
+ "get_backend",
14
+ "install_tool",
15
+ "uninstall_tool",
16
+ ]