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/registry.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Registry for short name -> spec resolution.
|
|
2
|
+
|
|
3
|
+
`ixt tool install ripgrep` -> lookup registry -> `@gh:BurntSushi/ripgrep` -> binary backend.
|
|
4
|
+
|
|
5
|
+
Loads embedded registry.toml + optional user overrides from
|
|
6
|
+
$IXT_HOME/config/registry.toml + optional high-priority files from
|
|
7
|
+
IXT_REGISTRY.
|
|
8
|
+
|
|
9
|
+
Format:
|
|
10
|
+
|
|
11
|
+
[tools]
|
|
12
|
+
ripgrep = "@gh:BurntSushi/ripgrep"
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import os
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ixt.config.toml import _load_toml
|
|
24
|
+
from ixt.data import get_data_path
|
|
25
|
+
|
|
26
|
+
REGISTRY_ENV = "IXT_REGISTRY"
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Data model
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class RegistryEntry:
|
|
35
|
+
"""A single tool entry in the registry."""
|
|
36
|
+
|
|
37
|
+
spec: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Loading
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_registry(data: dict[str, Any]) -> dict[str, RegistryEntry]:
|
|
46
|
+
"""Build a name -> RegistryEntry mapping from parsed TOML dict."""
|
|
47
|
+
tools = data.get("tools", {})
|
|
48
|
+
result: dict[str, RegistryEntry] = {}
|
|
49
|
+
for name, value in tools.items():
|
|
50
|
+
if isinstance(value, str):
|
|
51
|
+
result[name] = RegistryEntry(spec=value)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@functools.lru_cache(maxsize=1)
|
|
56
|
+
def load_registry(user_path: Path | None = None) -> dict[str, RegistryEntry]:
|
|
57
|
+
"""Load registry sources in priority order.
|
|
58
|
+
|
|
59
|
+
Lower-priority sources are loaded first. Later files override earlier
|
|
60
|
+
entries, so env-supplied registries have the highest priority.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
user_path: Explicit path to user override file.
|
|
64
|
+
If None, checks $IXT_HOME/config/registry.toml.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict mapping short name to RegistryEntry.
|
|
68
|
+
"""
|
|
69
|
+
registry: dict[str, RegistryEntry] = {}
|
|
70
|
+
_merge_registry_file(registry, get_data_path("registry.toml"))
|
|
71
|
+
|
|
72
|
+
# User overrides
|
|
73
|
+
if user_path is None:
|
|
74
|
+
from ixt.config.settings import get_ixt_config_dir
|
|
75
|
+
|
|
76
|
+
user_path = get_ixt_config_dir() / "registry.toml"
|
|
77
|
+
|
|
78
|
+
_merge_registry_file(registry, user_path)
|
|
79
|
+
|
|
80
|
+
for raw in _split_env_paths(os.environ.get(REGISTRY_ENV, "")):
|
|
81
|
+
if raw.startswith(("http://", "https://")):
|
|
82
|
+
continue
|
|
83
|
+
_merge_registry_file(registry, Path(raw).expanduser())
|
|
84
|
+
|
|
85
|
+
return registry
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _merge_registry_file(registry: dict[str, RegistryEntry], path: Path) -> None:
|
|
89
|
+
if not path.is_file():
|
|
90
|
+
return
|
|
91
|
+
registry.update(_parse_registry(_load_toml(path)))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _split_env_paths(value: str) -> list[str]:
|
|
95
|
+
return [part for part in (item.strip() for item in value.split(";")) if part]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Lookup
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def lookup(name: str, registry: dict[str, RegistryEntry] | None = None) -> RegistryEntry | None:
|
|
104
|
+
"""Resolve a short name to a RegistryEntry.
|
|
105
|
+
|
|
106
|
+
Lookup strategy:
|
|
107
|
+
1. Exact match on registry key (e.g. "ripgrep" -> RegistryEntry)
|
|
108
|
+
2. Match on repo part of owner/repo (e.g. "nushell" matches "nushell/nushell")
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
name: Short tool name to look up.
|
|
112
|
+
registry: Registry dict. Loads defaults if None.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
RegistryEntry, or None if not found.
|
|
116
|
+
"""
|
|
117
|
+
if registry is None:
|
|
118
|
+
registry = load_registry()
|
|
119
|
+
|
|
120
|
+
lower = name.lower()
|
|
121
|
+
|
|
122
|
+
# 1. Exact key match (case-insensitive)
|
|
123
|
+
for key, entry in registry.items():
|
|
124
|
+
if key.lower() == lower:
|
|
125
|
+
return entry
|
|
126
|
+
|
|
127
|
+
# 2. Match on repo name part (last segment after /)
|
|
128
|
+
for entry in registry.values():
|
|
129
|
+
# Strip @prefix: if present to get the raw spec
|
|
130
|
+
raw = entry.spec.partition(":")[2] if ":" in entry.spec else entry.spec
|
|
131
|
+
if "/" in raw:
|
|
132
|
+
repo_name = raw.rsplit("/", 1)[1]
|
|
133
|
+
if repo_name.lower() == lower:
|
|
134
|
+
return entry
|
|
135
|
+
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def lookup_spec(name: str, registry: dict[str, RegistryEntry] | None = None) -> str | None:
|
|
140
|
+
"""Resolve a short name to a spec string (e.g. ``@gh:BurntSushi/ripgrep``).
|
|
141
|
+
|
|
142
|
+
Convenience wrapper around :func:`lookup` that returns just the spec.
|
|
143
|
+
"""
|
|
144
|
+
entry = lookup(name, registry)
|
|
145
|
+
return entry.spec if entry else None
|
ixt/config/settings.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Path management and global configuration for ixt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ixt.platform import get_binary_extension
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_ixt_home() -> Path:
|
|
12
|
+
"""Get the ixt home directory.
|
|
13
|
+
|
|
14
|
+
Resolution order:
|
|
15
|
+
|
|
16
|
+
1. ``$IXT_HOME`` if set — explicit user override.
|
|
17
|
+
2. ``$XDG_DATA_HOME/ixt`` if ``XDG_DATA_HOME`` is set.
|
|
18
|
+
3. ``~/.local/share/ixt`` otherwise (XDG default).
|
|
19
|
+
|
|
20
|
+
The home directory stores user-visible ixt state: ``config/`` and
|
|
21
|
+
``installed/``. Regenerable cache content lives under
|
|
22
|
+
:func:`get_ixt_cache_home`.
|
|
23
|
+
"""
|
|
24
|
+
if env_home := os.environ.get("IXT_HOME"):
|
|
25
|
+
return Path(env_home).expanduser()
|
|
26
|
+
if xdg_data := os.environ.get("XDG_DATA_HOME"):
|
|
27
|
+
return Path(xdg_data).expanduser() / "ixt"
|
|
28
|
+
return Path.home() / ".local" / "share" / "ixt"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_ixt_installed_dir() -> Path:
|
|
32
|
+
"""Get the active installation root (``$IXT_HOME/installed``)."""
|
|
33
|
+
return get_ixt_home() / "installed"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_ixt_bin_dir() -> Path:
|
|
37
|
+
"""Get the directory for exposed tool shims."""
|
|
38
|
+
if env_bin := os.environ.get("IXT_BIN_DIR"):
|
|
39
|
+
return Path(env_bin).expanduser()
|
|
40
|
+
return get_ixt_installed_dir() / "bin"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_ixt_envs_dir() -> Path:
|
|
44
|
+
"""Get the directory containing isolated environments per tool."""
|
|
45
|
+
return get_ixt_installed_dir() / "envs"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_ixt_runtimes_dir() -> Path:
|
|
49
|
+
"""Get the directory for bootstrapped runtimes (uv, bun, python)."""
|
|
50
|
+
return get_ixt_installed_dir() / "runtimes"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_ixt_config_dir() -> Path:
|
|
54
|
+
"""Get the directory for user-editable ixt tool config files."""
|
|
55
|
+
return get_ixt_home() / "config"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_ixt_cache_home() -> Path:
|
|
59
|
+
"""Get the ixt cache root.
|
|
60
|
+
|
|
61
|
+
Resolution order:
|
|
62
|
+
|
|
63
|
+
1. ``$IXT_CACHE_HOME`` if set — explicit ixt override.
|
|
64
|
+
2. ``$XDG_CACHE_HOME/ixt`` if ``XDG_CACHE_HOME`` is set.
|
|
65
|
+
3. ``~/.cache/ixt`` otherwise (XDG default).
|
|
66
|
+
"""
|
|
67
|
+
if env_cache := os.environ.get("IXT_CACHE_HOME"):
|
|
68
|
+
return Path(env_cache).expanduser()
|
|
69
|
+
if xdg_cache := os.environ.get("XDG_CACHE_HOME"):
|
|
70
|
+
return Path(xdg_cache).expanduser() / "ixt"
|
|
71
|
+
return Path.home() / ".cache" / "ixt"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_ixt_downloads_dir() -> Path:
|
|
75
|
+
"""Get the cache directory for network artifacts."""
|
|
76
|
+
return get_ixt_cache_home() / "downloads"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_ixt_metadata_dir() -> Path:
|
|
80
|
+
"""Get the cache directory for metadata and resolution state."""
|
|
81
|
+
return get_ixt_cache_home() / "metadata"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_ixt_tmp_dir() -> Path:
|
|
85
|
+
"""Get the cache directory for ixt-owned temporary files."""
|
|
86
|
+
return get_ixt_cache_home() / "tmp"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_ixt_cache_dir() -> Path:
|
|
90
|
+
"""Get the network-artifact cache directory.
|
|
91
|
+
|
|
92
|
+
Kept as the historical helper name for backend code that stores
|
|
93
|
+
downloaded release assets and fetched ``ixt.setup.toml`` files.
|
|
94
|
+
"""
|
|
95
|
+
return get_ixt_downloads_dir()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Settings:
|
|
99
|
+
"""Central, backend-agnostic configuration for ixt."""
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self.home = get_ixt_home()
|
|
103
|
+
self.installed_dir = get_ixt_installed_dir()
|
|
104
|
+
self.bin_dir = get_ixt_bin_dir()
|
|
105
|
+
self.envs_dir = get_ixt_envs_dir()
|
|
106
|
+
self.runtimes_dir = get_ixt_runtimes_dir()
|
|
107
|
+
self.config_dir = get_ixt_config_dir()
|
|
108
|
+
self.cache_home = get_ixt_cache_home()
|
|
109
|
+
self.downloads_dir = get_ixt_downloads_dir()
|
|
110
|
+
self.metadata_dir = get_ixt_metadata_dir()
|
|
111
|
+
self.tmp_dir = get_ixt_tmp_dir()
|
|
112
|
+
self.cache_dir = get_ixt_cache_dir()
|
|
113
|
+
|
|
114
|
+
def ensure_directories(self):
|
|
115
|
+
"""Create all necessary ixt state and cache directories."""
|
|
116
|
+
for d in [
|
|
117
|
+
self.home,
|
|
118
|
+
self.config_dir,
|
|
119
|
+
self.installed_dir,
|
|
120
|
+
self.bin_dir,
|
|
121
|
+
self.envs_dir,
|
|
122
|
+
self.runtimes_dir,
|
|
123
|
+
self.cache_home,
|
|
124
|
+
self.downloads_dir,
|
|
125
|
+
self.metadata_dir,
|
|
126
|
+
self.tmp_dir,
|
|
127
|
+
self.cache_dir,
|
|
128
|
+
]:
|
|
129
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def uv_runtime(self) -> Path:
|
|
133
|
+
"""Path to the bootstrapped uv binary."""
|
|
134
|
+
return self.runtimes_dir / f"uv{get_binary_extension()}"
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def bun_runtime(self) -> Path:
|
|
138
|
+
"""Path to the bootstrapped bun binary."""
|
|
139
|
+
return self.runtimes_dir / f"bun{get_binary_extension()}"
|
|
140
|
+
|
|
141
|
+
def get_tool_env_dir(self, tool_name: str) -> Path:
|
|
142
|
+
"""Get the isolated environment directory for a specific tool."""
|
|
143
|
+
safe = tool_name.replace("@", "").replace("/", "-")
|
|
144
|
+
return self.envs_dir / safe
|
|
145
|
+
|
|
146
|
+
def get_tool_metadata_file(self, tool_name: str) -> Path:
|
|
147
|
+
"""Get the ixt.json metadata file for a tool."""
|
|
148
|
+
return self.get_tool_env_dir(tool_name) / "ixt.json"
|
|
149
|
+
|
|
150
|
+
def iter_installed_metadata(self) -> list[Path]:
|
|
151
|
+
"""Return sorted list of ixt.json paths for all installed tools."""
|
|
152
|
+
if not self.envs_dir.exists():
|
|
153
|
+
return []
|
|
154
|
+
results = []
|
|
155
|
+
for entry in sorted(self.envs_dir.iterdir()):
|
|
156
|
+
if entry.name.startswith(".") or not entry.is_dir():
|
|
157
|
+
continue
|
|
158
|
+
meta = entry / "ixt.json"
|
|
159
|
+
if meta.exists():
|
|
160
|
+
results.append(meta)
|
|
161
|
+
return results
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Global settings singleton
|
|
165
|
+
_settings: Settings | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_settings() -> Settings:
|
|
169
|
+
"""Get or create the global Settings instance."""
|
|
170
|
+
global _settings
|
|
171
|
+
if _settings is None:
|
|
172
|
+
_settings = Settings()
|
|
173
|
+
return _settings
|
ixt/config/setup_toml.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Parser for ``ixt.setup.toml`` — metadata defined by tool authors.
|
|
2
|
+
|
|
3
|
+
A repo can contain an ``ixt.setup.toml`` at its root, which ixt reads
|
|
4
|
+
during installation to discover how the tool should be installed and
|
|
5
|
+
exposed. This avoids relying solely on heuristics or the built-in registry.
|
|
6
|
+
|
|
7
|
+
Priority: ixt.setup.toml > heuristic auto-detection.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ixt.net.source import ReleaseSource
|
|
19
|
+
|
|
20
|
+
if sys.version_info >= (3, 11):
|
|
21
|
+
import tomllib
|
|
22
|
+
|
|
23
|
+
_loads = tomllib.loads
|
|
24
|
+
else:
|
|
25
|
+
from ixt.config.toml import _mini_toml_parse
|
|
26
|
+
|
|
27
|
+
_loads = _mini_toml_parse
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Data model
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class HookConfig:
|
|
37
|
+
"""A lifecycle hook (post_install or pre_uninstall)."""
|
|
38
|
+
|
|
39
|
+
run: str | None = None
|
|
40
|
+
check: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class SetupConfig:
|
|
45
|
+
"""Parsed ``ixt.setup.toml`` from a tool repo."""
|
|
46
|
+
|
|
47
|
+
name: str | None = None
|
|
48
|
+
expose: list[str] = field(default_factory=list)
|
|
49
|
+
description: str | None = None
|
|
50
|
+
asset_pattern: str | None = None
|
|
51
|
+
|
|
52
|
+
completions: dict[str, str] = field(default_factory=dict)
|
|
53
|
+
man: dict[str, str] = field(default_factory=dict)
|
|
54
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
post_install: HookConfig = field(default_factory=HookConfig)
|
|
57
|
+
pre_uninstall: HookConfig = field(default_factory=HookConfig)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Parsing
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse_toml_string(text: str) -> dict[str, Any]:
|
|
66
|
+
"""Parse a TOML string using stdlib (3.11+) or fallback parser."""
|
|
67
|
+
return _loads(text)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_setup_toml(text: str) -> SetupConfig:
|
|
71
|
+
"""Parse the text content of an ``ixt.setup.toml`` file.
|
|
72
|
+
|
|
73
|
+
The file is expected to have an ``[ixt]`` top-level table.
|
|
74
|
+
Returns a SetupConfig with all recognized fields, or an empty
|
|
75
|
+
SetupConfig if the ``[ixt]`` section is absent.
|
|
76
|
+
"""
|
|
77
|
+
data = _parse_toml_string(text)
|
|
78
|
+
ixt = data.get("ixt", {})
|
|
79
|
+
if not isinstance(ixt, dict):
|
|
80
|
+
return SetupConfig()
|
|
81
|
+
|
|
82
|
+
expose_raw = ixt.get("expose")
|
|
83
|
+
if isinstance(expose_raw, str):
|
|
84
|
+
expose = [expose_raw]
|
|
85
|
+
elif isinstance(expose_raw, list):
|
|
86
|
+
expose = [str(e) for e in expose_raw]
|
|
87
|
+
else:
|
|
88
|
+
expose = []
|
|
89
|
+
|
|
90
|
+
completions = ixt.get("completions", {})
|
|
91
|
+
if not isinstance(completions, dict):
|
|
92
|
+
completions = {}
|
|
93
|
+
|
|
94
|
+
man = ixt.get("man", {})
|
|
95
|
+
if not isinstance(man, dict):
|
|
96
|
+
man = {}
|
|
97
|
+
|
|
98
|
+
meta = ixt.get("meta", {})
|
|
99
|
+
if not isinstance(meta, dict):
|
|
100
|
+
meta = {}
|
|
101
|
+
|
|
102
|
+
post_install = _parse_hook(ixt.get("post_install"))
|
|
103
|
+
pre_uninstall = _parse_hook(ixt.get("pre_uninstall"))
|
|
104
|
+
|
|
105
|
+
return SetupConfig(
|
|
106
|
+
name=ixt.get("name"),
|
|
107
|
+
expose=expose,
|
|
108
|
+
description=ixt.get("description"),
|
|
109
|
+
asset_pattern=ixt.get("asset_pattern"),
|
|
110
|
+
completions=completions,
|
|
111
|
+
man=man,
|
|
112
|
+
meta=meta,
|
|
113
|
+
post_install=post_install,
|
|
114
|
+
pre_uninstall=pre_uninstall,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _parse_hook(raw: Any) -> HookConfig:
|
|
119
|
+
"""Parse a hook section (dict with optional run/check keys)."""
|
|
120
|
+
if not isinstance(raw, dict):
|
|
121
|
+
return HookConfig()
|
|
122
|
+
return HookConfig(
|
|
123
|
+
run=raw.get("run"),
|
|
124
|
+
check=raw.get("check"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Fetch and cache
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_local_setup_toml(path: Path) -> SetupConfig:
|
|
134
|
+
"""Load an ``ixt.setup.toml`` from a local file path.
|
|
135
|
+
|
|
136
|
+
Raises FileNotFoundError if *path* does not exist.
|
|
137
|
+
"""
|
|
138
|
+
text = path.read_text(encoding="utf-8")
|
|
139
|
+
return parse_setup_toml(text)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def fetch_setup_toml(
|
|
143
|
+
source: ReleaseSource,
|
|
144
|
+
owner: str,
|
|
145
|
+
repo: str,
|
|
146
|
+
*,
|
|
147
|
+
cache_dir: Path | None = None,
|
|
148
|
+
ref: str | None = None,
|
|
149
|
+
) -> SetupConfig | None:
|
|
150
|
+
"""Fetch ``ixt.setup.toml`` from the repo and parse it.
|
|
151
|
+
|
|
152
|
+
Uses the ReleaseSource's ``get_file_content()`` method. When *ref* is
|
|
153
|
+
given, the file is fetched at that specific revision (tag) so the
|
|
154
|
+
config stays coherent with the installed release.
|
|
155
|
+
Caches the result to disk if *cache_dir* is given.
|
|
156
|
+
Returns None if the file doesn't exist (404).
|
|
157
|
+
"""
|
|
158
|
+
# Check cache first
|
|
159
|
+
if cache_dir is not None:
|
|
160
|
+
cached = cache_dir / "setup.toml"
|
|
161
|
+
try:
|
|
162
|
+
text = cached.read_text(encoding="utf-8")
|
|
163
|
+
if text:
|
|
164
|
+
return parse_setup_toml(text)
|
|
165
|
+
except FileNotFoundError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
# Fetch from repo
|
|
169
|
+
text = source.get_file_content(owner, repo, "ixt.setup.toml", ref=ref)
|
|
170
|
+
if text is None:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# Cache for later
|
|
174
|
+
if cache_dir is not None:
|
|
175
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
cached = cache_dir / "setup.toml"
|
|
177
|
+
cached.write_text(text, encoding="utf-8")
|
|
178
|
+
|
|
179
|
+
return parse_setup_toml(text)
|