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/cli/cmd_misc.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Standalone utility subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from ixt.config.models import ToolRecord
|
|
11
|
+
from ixt.config.settings import get_settings
|
|
12
|
+
from ixt.core.install import resolve_tool_arg
|
|
13
|
+
from ixt.libs.logger import get_logger
|
|
14
|
+
from ixt.libs.style import bold, cyan, dim, green, yellow
|
|
15
|
+
from ixt.platform import get_platform_info
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cmd_setup_path(args: argparse.Namespace) -> int:
|
|
19
|
+
from ixt.core.setup_path import setup_path
|
|
20
|
+
|
|
21
|
+
logger = get_logger("setup-path")
|
|
22
|
+
result = setup_path(check_only=args.check, shell=getattr(args, "shell", None))
|
|
23
|
+
if result.warning:
|
|
24
|
+
logger.warn(result.warning)
|
|
25
|
+
logger.info(result.message)
|
|
26
|
+
if result.snippet:
|
|
27
|
+
logger.info("")
|
|
28
|
+
for line in result.snippet.rstrip("\n").splitlines():
|
|
29
|
+
logger.info(f" {line}")
|
|
30
|
+
return 0 if result.status in ("ok", "ok_restart", "added", "migrated") else 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_setup_completions(args: argparse.Namespace) -> int:
|
|
34
|
+
from ixt.core.setup_completions import generate_completion
|
|
35
|
+
|
|
36
|
+
script = generate_completion(args.shell)
|
|
37
|
+
sys.stdout.write(script)
|
|
38
|
+
if not script.endswith("\n"):
|
|
39
|
+
sys.stdout.write("\n")
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_runtime_upgrade(args: argparse.Namespace) -> int:
|
|
44
|
+
from ixt.core.bootstrap import BootstrapError, RuntimeName, upgrade_runtime
|
|
45
|
+
|
|
46
|
+
logger = get_logger("runtime")
|
|
47
|
+
target = getattr(args, "name", None)
|
|
48
|
+
if not target:
|
|
49
|
+
logger.error("Specify a runtime to upgrade (e.g. 'ixt runtime upgrade uv')")
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
names = ("uv", "bun") if target == "all" else (target,)
|
|
53
|
+
ok = True
|
|
54
|
+
for name in names:
|
|
55
|
+
try:
|
|
56
|
+
path = upgrade_runtime(cast(RuntimeName, name))
|
|
57
|
+
except BootstrapError as exc:
|
|
58
|
+
logger.error(f"Failed to upgrade {name}: {exc}")
|
|
59
|
+
ok = False
|
|
60
|
+
continue
|
|
61
|
+
logger.info(f" {green('ok')} {name} upgraded at {dim(path)}")
|
|
62
|
+
return 0 if ok else 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_setup(args: argparse.Namespace) -> int:
|
|
66
|
+
"""Dispatch ``ixt setup <subcommand>``."""
|
|
67
|
+
sub = getattr(args, "setup_command", None)
|
|
68
|
+
if sub == "path":
|
|
69
|
+
return cmd_setup_path(args)
|
|
70
|
+
if sub == "completions":
|
|
71
|
+
return cmd_setup_completions(args)
|
|
72
|
+
logger = get_logger("setup")
|
|
73
|
+
logger.error("Specify a setup subcommand (e.g. 'ixt setup path')")
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_runtimes(args: argparse.Namespace) -> int:
|
|
78
|
+
"""Dispatch ``ixt runtime <subcommand>``."""
|
|
79
|
+
action = getattr(args, "runtimes_action", None)
|
|
80
|
+
if action == "info":
|
|
81
|
+
return _cmd_runtimes_info(args)
|
|
82
|
+
if action == "prune":
|
|
83
|
+
return _cmd_runtimes_prune(args)
|
|
84
|
+
if action == "upgrade":
|
|
85
|
+
return cmd_runtime_upgrade(args)
|
|
86
|
+
|
|
87
|
+
logger = get_logger("runtime")
|
|
88
|
+
logger.error("Specify a runtime subcommand (info, prune, upgrade)")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cmd_runtimes_info(args: argparse.Namespace) -> int:
|
|
93
|
+
from ixt.core.cache import humanize_size
|
|
94
|
+
from ixt.core.runtimes import runtime_statuses
|
|
95
|
+
|
|
96
|
+
_ = args
|
|
97
|
+
logger = get_logger("runtime")
|
|
98
|
+
settings = get_settings()
|
|
99
|
+
|
|
100
|
+
logger.info(f" {bold('ixt runtime')}")
|
|
101
|
+
logger.info(f" Root: {cyan(str(settings.runtimes_dir))}")
|
|
102
|
+
logger.info("")
|
|
103
|
+
for status in runtime_statuses(settings=settings):
|
|
104
|
+
state = green("present") if status.present else dim("missing")
|
|
105
|
+
use = yellow("used") if status.used else dim("unused")
|
|
106
|
+
version = f" {dim(status.version)}" if status.version else ""
|
|
107
|
+
logger.info(
|
|
108
|
+
f" {status.name:3s} {state:10s} {use:8s} "
|
|
109
|
+
f"{humanize_size(status.bytes):>8s} {dim(str(status.path))}{version}"
|
|
110
|
+
)
|
|
111
|
+
if status.used_by:
|
|
112
|
+
logger.info(f" {dim('used by: ' + ', '.join(status.used_by))}")
|
|
113
|
+
elif status.present:
|
|
114
|
+
logger.info(f" {dim('prunable: ' + status.prune_reason)}")
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _cmd_runtimes_prune(args: argparse.Namespace) -> int:
|
|
119
|
+
from ixt.core.cache import humanize_size
|
|
120
|
+
from ixt.core.runtimes import prune_runtimes
|
|
121
|
+
|
|
122
|
+
logger = get_logger("runtime")
|
|
123
|
+
results = prune_runtimes(all_=getattr(args, "all", False))
|
|
124
|
+
removed = [result for result in results if result.removed]
|
|
125
|
+
for result in results:
|
|
126
|
+
if result.removed:
|
|
127
|
+
logger.info(
|
|
128
|
+
f" {green('ok')} removed {result.name}: "
|
|
129
|
+
f"{humanize_size(result.bytes)}, {result.files} "
|
|
130
|
+
f"{'file' if result.files == 1 else 'files'} ({dim(str(result.path))})"
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
logger.info(f" {dim('--')} kept {result.name}: {result.reason}")
|
|
134
|
+
logger.info("")
|
|
135
|
+
logger.info(f" Removed: {bold(str(len(removed)))} runtime(s)")
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
140
|
+
from ixt.core.doctor import run_doctor
|
|
141
|
+
|
|
142
|
+
logger = get_logger("doctor")
|
|
143
|
+
|
|
144
|
+
pinfo = get_platform_info()
|
|
145
|
+
report = run_doctor(network=not getattr(args, "no_network", False))
|
|
146
|
+
|
|
147
|
+
logger.info(f" {bold('ixt doctor')}")
|
|
148
|
+
shell = next((c.detail for c in report.checks if c.name == "Shell"), "unknown")
|
|
149
|
+
logger.info(
|
|
150
|
+
f" Platform: {pinfo.os.value}/{pinfo.arch.value} "
|
|
151
|
+
f"Shell {shell} Python {pinfo.python_version}"
|
|
152
|
+
)
|
|
153
|
+
logger.info("")
|
|
154
|
+
|
|
155
|
+
for check in report.checks:
|
|
156
|
+
if check.ok:
|
|
157
|
+
logger.info(f" {green('ok')} {check.name:15s} {dim(check.detail)}")
|
|
158
|
+
else:
|
|
159
|
+
logger.info(f" {yellow('!!')} {check.name:15s} {check.detail}")
|
|
160
|
+
if check.hint:
|
|
161
|
+
logger.info(f" {dim('Hint: ' + check.hint)}")
|
|
162
|
+
|
|
163
|
+
logger.info("")
|
|
164
|
+
if report.ok:
|
|
165
|
+
logger.info(f" {green('All checks passed')}")
|
|
166
|
+
else:
|
|
167
|
+
n = len(report.warnings)
|
|
168
|
+
logger.info(f" {yellow(f'{n} warning(s)')}")
|
|
169
|
+
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_environment(args: argparse.Namespace) -> int:
|
|
174
|
+
from ixt.core.cache import humanize_size, storage_sizes
|
|
175
|
+
|
|
176
|
+
logger = get_logger("environment")
|
|
177
|
+
settings = get_settings()
|
|
178
|
+
pinfo = get_platform_info()
|
|
179
|
+
|
|
180
|
+
logger.info(f" {bold('ixt environment')}")
|
|
181
|
+
logger.info(f" Platform: {pinfo.os.value}/{pinfo.arch.value}")
|
|
182
|
+
logger.info(f" Python: {pinfo.python_version}")
|
|
183
|
+
logger.info("")
|
|
184
|
+
logger.info(f" Home: {cyan(str(settings.home))}")
|
|
185
|
+
logger.info(f" Config: {cyan(str(settings.config_dir))}")
|
|
186
|
+
logger.info(f" Installed: {cyan(str(settings.installed_dir))}")
|
|
187
|
+
logger.info(f" Bin dir: {cyan(str(settings.bin_dir))}")
|
|
188
|
+
logger.info(f" Envs dir: {cyan(str(settings.envs_dir))}")
|
|
189
|
+
logger.info(f" Runtimes: {cyan(str(settings.runtimes_dir))}")
|
|
190
|
+
logger.info(f" Cache: {cyan(str(settings.cache_home))}")
|
|
191
|
+
logger.info(f" Downloads: {cyan(str(settings.downloads_dir))}")
|
|
192
|
+
logger.info(f" Metadata: {cyan(str(settings.metadata_dir))}")
|
|
193
|
+
|
|
194
|
+
count = len(settings.iter_installed_metadata())
|
|
195
|
+
logger.info("")
|
|
196
|
+
logger.info(f" Tools: {bold(str(count))} installed")
|
|
197
|
+
|
|
198
|
+
if getattr(args, "sizes", False):
|
|
199
|
+
logger.info("")
|
|
200
|
+
logger.info(" Sizes:")
|
|
201
|
+
for entry in storage_sizes(settings=settings):
|
|
202
|
+
files = "file" if entry.files == 1 else "files"
|
|
203
|
+
logger.info(
|
|
204
|
+
f" {entry.name:10s} {humanize_size(entry.bytes):>8s} {entry.files} {files}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _get_installed_env_dir(tool: str, *, logger_name: str) -> Path | None:
|
|
211
|
+
logger = get_logger(logger_name)
|
|
212
|
+
settings = get_settings()
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
tool_name = resolve_tool_arg(tool, settings=settings)
|
|
216
|
+
except ValueError as e:
|
|
217
|
+
logger.error(str(e))
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
meta_file = settings.get_tool_metadata_file(tool_name)
|
|
221
|
+
|
|
222
|
+
if not meta_file.exists():
|
|
223
|
+
logger.error(f"Tool '{tool}' is not installed")
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
record = ToolRecord.load_json(meta_file)
|
|
227
|
+
env_dir = Path(record.env_dir).expanduser()
|
|
228
|
+
|
|
229
|
+
if not env_dir.is_dir():
|
|
230
|
+
logger.error(f"Environment directory missing: {env_dir}")
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
return env_dir
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def cmd_shell(args: argparse.Namespace) -> int:
|
|
237
|
+
import os
|
|
238
|
+
import subprocess # nosemgrep: gitlab.bandit.B404
|
|
239
|
+
|
|
240
|
+
logger = get_logger("shell")
|
|
241
|
+
env_dir = _get_installed_env_dir(args.tool, logger_name="shell")
|
|
242
|
+
if env_dir is None:
|
|
243
|
+
return 1
|
|
244
|
+
|
|
245
|
+
# Launch a subshell in the tool's environment directory
|
|
246
|
+
shell = os.environ.get("SHELL", "/bin/sh")
|
|
247
|
+
logger.info(f" Entering {bold(args.tool)} env ({dim(str(env_dir))})")
|
|
248
|
+
logger.info(f" {dim('Type exit to return')}")
|
|
249
|
+
# Safe: $SHELL is the intended source, exec as list (no shell interpretation),
|
|
250
|
+
# env_dir from stored ToolRecord.
|
|
251
|
+
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use-tainted-env-args.dangerous-subprocess-use-tainted-env-args # noqa: E501
|
|
252
|
+
result = subprocess.run([shell], cwd=env_dir)
|
|
253
|
+
return result.returncode
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cmd_where(args: argparse.Namespace) -> int:
|
|
257
|
+
env_dir = _get_installed_env_dir(args.tool, logger_name="where")
|
|
258
|
+
if env_dir is None:
|
|
259
|
+
return 1
|
|
260
|
+
sys.stdout.write(f"{env_dir.resolve()}\n")
|
|
261
|
+
return 0
|
ixt/cli/cmd_registry.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Registry inspection subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ixt.config.registry import load_registry
|
|
8
|
+
from ixt.libs.logger import get_logger
|
|
9
|
+
from ixt.libs.output import data
|
|
10
|
+
from ixt.libs.style import bold, dim
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_registry(args: argparse.Namespace) -> int:
|
|
14
|
+
"""Dispatch ``ixt registry <subcommand>``."""
|
|
15
|
+
action = getattr(args, "registry_action", None)
|
|
16
|
+
if action == "list":
|
|
17
|
+
return _cmd_registry_list(args)
|
|
18
|
+
|
|
19
|
+
logger = get_logger("registry")
|
|
20
|
+
logger.error("Specify a registry subcommand (list)")
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _cmd_registry_list(args: argparse.Namespace) -> int:
|
|
25
|
+
"""Print known short-name registry entries."""
|
|
26
|
+
_ = args
|
|
27
|
+
registry = load_registry()
|
|
28
|
+
if not registry:
|
|
29
|
+
data("No registry entries")
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
width = max(len(name) for name in registry)
|
|
33
|
+
for name, entry in sorted(registry.items()):
|
|
34
|
+
data(f" {bold(name.ljust(width))} {dim(entry.spec)}")
|
|
35
|
+
return 0
|
ixt/cli/cmd_upgrade.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""``ixt tool upgrade`` subcommand."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import nullcontext
|
|
8
|
+
|
|
9
|
+
from ixt.cli.parser import normalize_verbosity
|
|
10
|
+
from ixt.cli.render import format_source_tag, source_label_for_record
|
|
11
|
+
from ixt.config.models import ToolRecord
|
|
12
|
+
from ixt.config.settings import get_settings
|
|
13
|
+
from ixt.core.install import display_tool_name, resolve_tool_arg
|
|
14
|
+
from ixt.core.resolution_stats import collect_resolution_stats
|
|
15
|
+
from ixt.libs.logger import get_logger, get_verbosity
|
|
16
|
+
from ixt.libs.semver import compare_versions
|
|
17
|
+
from ixt.libs.style import bold, dim, green, red
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cmd_upgrade(args: argparse.Namespace) -> int:
|
|
21
|
+
normalize_verbosity(args)
|
|
22
|
+
logger = get_logger("upgrade")
|
|
23
|
+
dry_run = getattr(args, "dry_run", False)
|
|
24
|
+
use_cache = not getattr(args, "no_cache", False)
|
|
25
|
+
|
|
26
|
+
if dry_run:
|
|
27
|
+
return _cmd_upgrade_dry_run(args, logger, use_cache=use_cache)
|
|
28
|
+
|
|
29
|
+
from ixt.core.upgrade import (
|
|
30
|
+
ToolLocalInstallError,
|
|
31
|
+
ToolNotInstalledError,
|
|
32
|
+
ToolPinnedError,
|
|
33
|
+
upgrade_all,
|
|
34
|
+
upgrade_tool,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if args.upgrade_all:
|
|
38
|
+
settings = get_settings()
|
|
39
|
+
total = len(settings.iter_installed_metadata())
|
|
40
|
+
if not total:
|
|
41
|
+
logger.info("No tools installed")
|
|
42
|
+
return 0
|
|
43
|
+
up_to_date = upgraded = pinned = skipped = errors = 0
|
|
44
|
+
start = time.monotonic()
|
|
45
|
+
logger.info(f"checking {total} tool(s)")
|
|
46
|
+
with _resolution_stats_context(args) as resolution_stats:
|
|
47
|
+
for i, (name, result) in enumerate(
|
|
48
|
+
upgrade_all(settings=settings, use_cache=use_cache), 1
|
|
49
|
+
):
|
|
50
|
+
prefix = dim(f"[{i}/{total}]")
|
|
51
|
+
if isinstance(result, ToolPinnedError):
|
|
52
|
+
pinned += 1
|
|
53
|
+
logger.info(f"{prefix} {_pinned_msg(result, settings=settings)}")
|
|
54
|
+
elif isinstance(result, ToolLocalInstallError):
|
|
55
|
+
skipped += 1
|
|
56
|
+
display = _display_for_tool_name(name, settings)
|
|
57
|
+
styled_display = bold(display)
|
|
58
|
+
logger.info(
|
|
59
|
+
f"{prefix} {dim('=')} {dim('skipped')} "
|
|
60
|
+
f"{styled_display} {dim('(local install)')}"
|
|
61
|
+
)
|
|
62
|
+
elif isinstance(result, Exception):
|
|
63
|
+
errors += 1
|
|
64
|
+
logger.error(f"{prefix} {red('x')} {red('failed')} {name}: {result}")
|
|
65
|
+
else:
|
|
66
|
+
record, old_version = result
|
|
67
|
+
if old_version and record.version and old_version == record.version:
|
|
68
|
+
up_to_date += 1
|
|
69
|
+
else:
|
|
70
|
+
upgraded += 1
|
|
71
|
+
logger.info(f"{prefix} {_upgrade_msg(record, old_version)}")
|
|
72
|
+
logger.info(
|
|
73
|
+
_upgrade_all_footer(
|
|
74
|
+
up_to_date=up_to_date,
|
|
75
|
+
upgraded=upgraded,
|
|
76
|
+
pinned=pinned,
|
|
77
|
+
skipped=skipped,
|
|
78
|
+
failed=errors,
|
|
79
|
+
elapsed=time.monotonic() - start,
|
|
80
|
+
use_cache=use_cache,
|
|
81
|
+
settings=settings,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
_log_resolution_summary(logger, resolution_stats)
|
|
85
|
+
return 1 if errors else 0
|
|
86
|
+
|
|
87
|
+
if not args.tool:
|
|
88
|
+
logger.error("Specify a tool name or use --all")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
tool_name = resolve_tool_arg(args.tool)
|
|
93
|
+
except ValueError as e:
|
|
94
|
+
logger.error(str(e))
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
record, old_version = upgrade_tool(tool_name, use_cache=use_cache)
|
|
99
|
+
except ToolNotInstalledError:
|
|
100
|
+
logger.error(f"Tool '{args.tool}' is not installed")
|
|
101
|
+
logger.error(f" {dim('Hint: run ixt tool list to see installed tools')}")
|
|
102
|
+
return 1
|
|
103
|
+
except ToolPinnedError as exc:
|
|
104
|
+
logger.info(_pinned_msg(exc, settings=get_settings()))
|
|
105
|
+
return 0
|
|
106
|
+
except ToolLocalInstallError as exc:
|
|
107
|
+
logger.error(str(exc))
|
|
108
|
+
return 1
|
|
109
|
+
except ValueError as exc:
|
|
110
|
+
logger.error(str(exc))
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
logger.info(_upgrade_msg(record, old_version))
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fmt_ttl(seconds: int) -> str:
|
|
118
|
+
if seconds <= 0:
|
|
119
|
+
return "off"
|
|
120
|
+
return f"{seconds // 60}m" if seconds % 60 == 0 else f"{seconds}s"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _fmt_remaining(seconds: int) -> str:
|
|
124
|
+
return f"~{seconds // 60}m" if seconds >= 60 else "<1m"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _cache_tail(use_cache: bool, settings) -> str:
|
|
128
|
+
"""Footer suffix describing the cache policy (or the --no-cache override)."""
|
|
129
|
+
if not use_cache:
|
|
130
|
+
return f" {dim('· fresh (--no-cache)')}"
|
|
131
|
+
from ixt.core.resolve_cache import min_remaining_ttl, ttl_seconds
|
|
132
|
+
|
|
133
|
+
ttl = ttl_seconds()
|
|
134
|
+
if ttl <= 0:
|
|
135
|
+
return ""
|
|
136
|
+
remaining = min_remaining_ttl(settings)
|
|
137
|
+
note = (
|
|
138
|
+
f"cached, expires in {_fmt_remaining(remaining)}"
|
|
139
|
+
if remaining is not None
|
|
140
|
+
else f"cache TTL {_fmt_ttl(ttl)}"
|
|
141
|
+
)
|
|
142
|
+
return f" {dim(f'· {note} · --no-cache to refresh')}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _upgrade_all_footer(
|
|
146
|
+
*,
|
|
147
|
+
up_to_date: int,
|
|
148
|
+
upgraded: int,
|
|
149
|
+
pinned: int,
|
|
150
|
+
skipped: int,
|
|
151
|
+
failed: int,
|
|
152
|
+
elapsed: float,
|
|
153
|
+
use_cache: bool,
|
|
154
|
+
settings,
|
|
155
|
+
) -> str:
|
|
156
|
+
labels = [
|
|
157
|
+
(up_to_date, "up-to-date"),
|
|
158
|
+
(upgraded, "upgraded"),
|
|
159
|
+
(pinned, "pinned"),
|
|
160
|
+
(skipped, "skipped"),
|
|
161
|
+
(failed, "failed"),
|
|
162
|
+
]
|
|
163
|
+
parts = [f"{n} {label}" for n, label in labels if n]
|
|
164
|
+
summary = ", ".join(parts) if parts else "nothing to do"
|
|
165
|
+
return f"{summary} {dim(f'in {elapsed:.1f}s')}{_cache_tail(use_cache, settings)}"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _resolution_stats_context(args: argparse.Namespace | None = None):
|
|
169
|
+
verbosity = max(get_verbosity(), getattr(args, "verbose", 0) if args is not None else 0)
|
|
170
|
+
if verbosity > 0:
|
|
171
|
+
return collect_resolution_stats()
|
|
172
|
+
return nullcontext(None)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _log_resolution_summary(logger, stats) -> None:
|
|
176
|
+
if stats is None or not stats.has_activity():
|
|
177
|
+
return
|
|
178
|
+
summary = stats.format_summary()
|
|
179
|
+
if summary:
|
|
180
|
+
logger.info(f"resolution: {summary}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _record_for_tool_name(name: str, settings) -> ToolRecord | None:
|
|
184
|
+
try:
|
|
185
|
+
return ToolRecord.load_json(settings.get_tool_metadata_file(name))
|
|
186
|
+
except (FileNotFoundError, ValueError):
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _display_for_tool_name(name: str, settings) -> str:
|
|
191
|
+
record = _record_for_tool_name(name, settings)
|
|
192
|
+
return display_tool_name(record, settings) if record is not None else name
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _source_suffix(record: ToolRecord) -> str:
|
|
196
|
+
tag = format_source_tag(source_label_for_record(record))
|
|
197
|
+
return f" {tag}" if tag else ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _pinned_msg(err, *, settings=None) -> str:
|
|
201
|
+
"""Format the info line shown when upgrade is skipped due to an == pin."""
|
|
202
|
+
tail = ""
|
|
203
|
+
if err.latest_available:
|
|
204
|
+
tail = f" {dim('latest:')} {err.latest_available}"
|
|
205
|
+
record = _record_for_tool_name(err.tool_name, settings) if settings is not None else None
|
|
206
|
+
display = display_tool_name(record, settings) if record is not None else err.tool_name
|
|
207
|
+
source = _source_suffix(record) if record is not None else ""
|
|
208
|
+
return (
|
|
209
|
+
f"{dim('=')} {dim('pinned')} {bold(display)} "
|
|
210
|
+
f"{dim(err.pinned_version)}{source} {dim('(ixt.toml)')}{tail}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _resolve_spec_for_record(record: ToolRecord) -> str:
|
|
215
|
+
"""Build the resolver spec from a stored record (strips @tag for binary)."""
|
|
216
|
+
if record.backend == "binary":
|
|
217
|
+
from ixt.net.source import strip_version_suffix
|
|
218
|
+
|
|
219
|
+
return strip_version_suffix(record.spec)
|
|
220
|
+
# Python/Node: registry queries need the underlying package name
|
|
221
|
+
# (``record.name`` is the installed id and may include a slot prefix).
|
|
222
|
+
return record.pkg()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _dry_run_upgrade_line(record: ToolRecord, new_version: str | None) -> str:
|
|
226
|
+
old = record.version or "unknown"
|
|
227
|
+
source = _source_suffix(record)
|
|
228
|
+
display = display_tool_name(record)
|
|
229
|
+
if new_version is None:
|
|
230
|
+
return f" {red('?')} {red('resolve failed')} {bold(display)} {dim(old)}{source}"
|
|
231
|
+
if compare_versions(old, new_version) == 0:
|
|
232
|
+
return f" {dim('=')} {dim('up-to-date')} {bold(display)} {dim(old)}{source}"
|
|
233
|
+
return (
|
|
234
|
+
f" {green('^')} {dim('would upgrade')} {bold(display)} "
|
|
235
|
+
f"{dim(old)} -> {dim(new_version)}{source}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _cmd_upgrade_dry_run(args: argparse.Namespace, logger, *, use_cache: bool = True) -> int:
|
|
240
|
+
from ixt.core.backend import BackendType
|
|
241
|
+
from ixt.core.resolve import resolve_latest_many
|
|
242
|
+
from ixt.core.upgrade import load_config_pins
|
|
243
|
+
|
|
244
|
+
settings = get_settings()
|
|
245
|
+
|
|
246
|
+
if args.upgrade_all:
|
|
247
|
+
meta_files = settings.iter_installed_metadata()
|
|
248
|
+
else:
|
|
249
|
+
if not args.tool:
|
|
250
|
+
logger.error("Specify a tool name or use --all")
|
|
251
|
+
return 1
|
|
252
|
+
try:
|
|
253
|
+
tool_name = resolve_tool_arg(args.tool, settings=settings)
|
|
254
|
+
except ValueError as e:
|
|
255
|
+
logger.error(str(e))
|
|
256
|
+
return 1
|
|
257
|
+
meta_file = settings.get_tool_metadata_file(tool_name)
|
|
258
|
+
if not meta_file.exists():
|
|
259
|
+
logger.error(f"Tool '{args.tool}' is not installed")
|
|
260
|
+
logger.error(f" {dim('Hint: run ixt tool list to see installed tools')}")
|
|
261
|
+
return 1
|
|
262
|
+
meta_files = [meta_file]
|
|
263
|
+
|
|
264
|
+
if not meta_files:
|
|
265
|
+
logger.info(f"{dim('[dry-run]')} no tools installed")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
records = [ToolRecord.load_json(meta_file) for meta_file in meta_files]
|
|
269
|
+
# Parse ixt.toml once for all tools, then resolve every latest in parallel.
|
|
270
|
+
pins = load_config_pins(record.name for record in records)
|
|
271
|
+
invalid: set[str] = set()
|
|
272
|
+
requests: list[tuple[str, BackendType, str]] = []
|
|
273
|
+
for record in records:
|
|
274
|
+
try:
|
|
275
|
+
backend = BackendType(record.backend)
|
|
276
|
+
except ValueError:
|
|
277
|
+
invalid.add(record.name)
|
|
278
|
+
continue
|
|
279
|
+
requests.append((record.name, backend, _resolve_spec_for_record(record)))
|
|
280
|
+
start = time.monotonic()
|
|
281
|
+
with _resolution_stats_context(args) as resolution_stats:
|
|
282
|
+
resolved = resolve_latest_many(requests, use_cache=use_cache, settings=settings)
|
|
283
|
+
elapsed = time.monotonic() - start
|
|
284
|
+
|
|
285
|
+
logger.info(f"{dim('[dry-run]')} would upgrade {len(records)} tool(s):")
|
|
286
|
+
any_failed = False
|
|
287
|
+
for record in records:
|
|
288
|
+
if record.name in invalid:
|
|
289
|
+
any_failed = True
|
|
290
|
+
logger.info(_dry_run_upgrade_line(record, None))
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
pinned = pins.get(record.name)
|
|
294
|
+
new_version = resolved.get(record.name)
|
|
295
|
+
if new_version is None and pinned is None:
|
|
296
|
+
any_failed = True
|
|
297
|
+
|
|
298
|
+
if pinned is not None:
|
|
299
|
+
logger.info(_dry_run_pinned_line(record, pinned, new_version))
|
|
300
|
+
continue
|
|
301
|
+
logger.info(_dry_run_upgrade_line(record, new_version))
|
|
302
|
+
|
|
303
|
+
if args.upgrade_all:
|
|
304
|
+
logger.info(
|
|
305
|
+
f" {dim(f'checked {len(records)} in {elapsed:.1f}s')}"
|
|
306
|
+
f"{_cache_tail(use_cache, settings)}"
|
|
307
|
+
)
|
|
308
|
+
_log_resolution_summary(logger, resolution_stats)
|
|
309
|
+
if any_failed:
|
|
310
|
+
logger.info(f" {dim('hint: set GITHUB_TOKEN for higher GH rate limits')}")
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _dry_run_pinned_line(record: ToolRecord, pinned: str, latest: str | None) -> str:
|
|
315
|
+
source = _source_suffix(record)
|
|
316
|
+
tail = f" {dim('latest:')} {latest}" if latest else ""
|
|
317
|
+
return (
|
|
318
|
+
f" {dim('=')} {dim('pinned')} {bold(display_tool_name(record))} "
|
|
319
|
+
f"{dim(record.version or '?')} {dim(f'(pinned: {pinned})')}{source}{tail}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _upgrade_msg(record, old_version: str | None = None) -> str:
|
|
324
|
+
"""Format an upgrade result message."""
|
|
325
|
+
ver = record.version or ""
|
|
326
|
+
display = display_tool_name(record)
|
|
327
|
+
source = _source_suffix(record)
|
|
328
|
+
if old_version and ver and old_version == ver:
|
|
329
|
+
return f"{dim('=')} {dim('up-to-date')} {bold(display)} {dim(ver)}{source}"
|
|
330
|
+
if old_version and ver and old_version != ver:
|
|
331
|
+
return (
|
|
332
|
+
f"{green('^')} {dim('upgraded')} {bold(display)} "
|
|
333
|
+
f"{dim(old_version)} -> {dim(ver)}{source}"
|
|
334
|
+
)
|
|
335
|
+
version = f" {dim(ver)}" if ver else ""
|
|
336
|
+
return f"{green('^')} {dim('upgraded')} {bold(display)}{version}{source}"
|
ixt/cli/commands.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Command dispatch — re-exports each ``cmd_*`` from its dedicated module.
|
|
2
|
+
|
|
3
|
+
The actual implementations live in ``cli/cmd_install.py``,
|
|
4
|
+
``cli/cmd_upgrade.py``, ``cli/cmd_info.py``, ``cli/cmd_config.py``,
|
|
5
|
+
``cli/cmd_apply.py`` and ``cli/cmd_misc.py``. This module aggregates them
|
|
6
|
+
and exposes the ``COMMANDS`` dispatch table consumed by ``cli.main``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from ixt.cli.cmd_apply import cmd_apply, cmd_export
|
|
14
|
+
from ixt.cli.cmd_cache import cmd_cache
|
|
15
|
+
from ixt.cli.cmd_config import cmd_config
|
|
16
|
+
from ixt.cli.cmd_info import cmd_info, cmd_list
|
|
17
|
+
from ixt.cli.cmd_install import _log_not_found_hint, cmd_add, cmd_install, cmd_uninstall
|
|
18
|
+
from ixt.cli.cmd_misc import (
|
|
19
|
+
cmd_doctor,
|
|
20
|
+
cmd_environment,
|
|
21
|
+
cmd_runtimes,
|
|
22
|
+
cmd_setup,
|
|
23
|
+
cmd_shell,
|
|
24
|
+
cmd_where,
|
|
25
|
+
)
|
|
26
|
+
from ixt.cli.cmd_registry import cmd_registry
|
|
27
|
+
from ixt.cli.cmd_upgrade import cmd_upgrade
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"COMMANDS",
|
|
31
|
+
"_log_not_found_hint",
|
|
32
|
+
"cmd_add",
|
|
33
|
+
"cmd_apply",
|
|
34
|
+
"cmd_cache",
|
|
35
|
+
"cmd_config",
|
|
36
|
+
"cmd_doctor",
|
|
37
|
+
"cmd_environment",
|
|
38
|
+
"cmd_export",
|
|
39
|
+
"cmd_info",
|
|
40
|
+
"cmd_install",
|
|
41
|
+
"cmd_list",
|
|
42
|
+
"cmd_registry",
|
|
43
|
+
"cmd_runtimes",
|
|
44
|
+
"cmd_setup",
|
|
45
|
+
"cmd_shell",
|
|
46
|
+
"cmd_uninstall",
|
|
47
|
+
"cmd_upgrade",
|
|
48
|
+
"cmd_where",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
COMMANDS: dict[str, Callable] = {
|
|
53
|
+
"add": cmd_add,
|
|
54
|
+
"install": cmd_install,
|
|
55
|
+
"uninstall": cmd_uninstall,
|
|
56
|
+
"upgrade": cmd_upgrade,
|
|
57
|
+
"list": cmd_list,
|
|
58
|
+
"info": cmd_info,
|
|
59
|
+
"config": cmd_config,
|
|
60
|
+
"shell": cmd_shell,
|
|
61
|
+
"where": cmd_where,
|
|
62
|
+
"apply": cmd_apply,
|
|
63
|
+
"export": cmd_export,
|
|
64
|
+
"cache": cmd_cache,
|
|
65
|
+
"registry": cmd_registry,
|
|
66
|
+
"runtime": cmd_runtimes,
|
|
67
|
+
"setup": cmd_setup,
|
|
68
|
+
"environment": cmd_environment,
|
|
69
|
+
"doctor": cmd_doctor,
|
|
70
|
+
}
|