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.
- ixt/__init__.py +8 -0
- ixt/__main__.py +8 -0
- ixt/backends/__init__.py +1 -0
- ixt/backends/binary.py +935 -0
- ixt/backends/binary_resolver.py +307 -0
- ixt/backends/node.py +490 -0
- ixt/backends/python.py +234 -0
- ixt/cli/__init__.py +31 -0
- ixt/cli/argparse_completion.py +557 -0
- ixt/cli/cmd_apply.py +404 -0
- ixt/cli/cmd_cache.py +86 -0
- ixt/cli/cmd_config.py +295 -0
- ixt/cli/cmd_info.py +116 -0
- ixt/cli/cmd_install.py +508 -0
- ixt/cli/cmd_misc.py +261 -0
- ixt/cli/cmd_registry.py +35 -0
- ixt/cli/cmd_upgrade.py +336 -0
- ixt/cli/commands.py +70 -0
- ixt/cli/parser.py +555 -0
- ixt/cli/render.py +85 -0
- ixt/config/__init__.py +5 -0
- ixt/config/asset_index.py +305 -0
- ixt/config/asset_pattern_cache.py +87 -0
- ixt/config/env_policy.py +340 -0
- ixt/config/flags.py +29 -0
- ixt/config/fs_policy.py +17 -0
- ixt/config/heuristics.py +465 -0
- ixt/config/models.py +176 -0
- ixt/config/registry.py +145 -0
- ixt/config/settings.py +173 -0
- ixt/config/setup_toml.py +179 -0
- ixt/config/toml.py +416 -0
- ixt/core/__init__.py +16 -0
- ixt/core/apply.py +564 -0
- ixt/core/apply_actions.py +106 -0
- ixt/core/backend.py +187 -0
- ixt/core/bootstrap.py +410 -0
- ixt/core/cache.py +332 -0
- ixt/core/discover.py +150 -0
- ixt/core/doctor.py +591 -0
- ixt/core/export.py +419 -0
- ixt/core/expose.py +350 -0
- ixt/core/extract.py +261 -0
- ixt/core/hooks.py +182 -0
- ixt/core/identity.py +148 -0
- ixt/core/inject.py +143 -0
- ixt/core/install.py +509 -0
- ixt/core/install_local.py +229 -0
- ixt/core/locks.py +54 -0
- ixt/core/pathlink.py +86 -0
- ixt/core/resolution_stats.py +191 -0
- ixt/core/resolve.py +150 -0
- ixt/core/resolve_cache.py +185 -0
- ixt/core/runtimes.py +192 -0
- ixt/core/save.py +237 -0
- ixt/core/setup_completions.py +11 -0
- ixt/core/setup_path.py +368 -0
- ixt/core/upgrade.py +596 -0
- ixt/data/__init__.py +10 -0
- ixt/data/asset_index.json +574 -0
- ixt/data/heuristics.toml +98 -0
- ixt/data/registry.toml +71 -0
- ixt/libs/__init__.py +3 -0
- ixt/libs/constants.py +4 -0
- ixt/libs/fmt.py +108 -0
- ixt/libs/logger.py +109 -0
- ixt/libs/output.py +25 -0
- ixt/libs/req_spec.py +115 -0
- ixt/libs/semver.py +149 -0
- ixt/libs/shell.py +126 -0
- ixt/libs/style.py +238 -0
- ixt/net/__init__.py +1 -0
- ixt/net/github_api.py +158 -0
- ixt/net/gitlab_api.py +149 -0
- ixt/net/http.py +194 -0
- ixt/net/npm.py +24 -0
- ixt/net/pypi.py +26 -0
- ixt/net/source.py +163 -0
- ixt/platform/__init__.py +131 -0
- ixt/platform/win.py +68 -0
- ixt_cli-0.8.0.dist-info/METADATA +294 -0
- ixt_cli-0.8.0.dist-info/RECORD +84 -0
- ixt_cli-0.8.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|