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_apply.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""``ixt tool apply``, ``ixt tool export``, and ``ixt asset-index export`` handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
10
|
+
|
|
11
|
+
from ixt.cli.render import (
|
|
12
|
+
format_exposed_bins,
|
|
13
|
+
format_source_tag,
|
|
14
|
+
source_label_for_spec,
|
|
15
|
+
)
|
|
16
|
+
from ixt.config.models import ToolRecord
|
|
17
|
+
from ixt.config.settings import get_settings
|
|
18
|
+
from ixt.libs.logger import get_logger
|
|
19
|
+
from ixt.libs.output import data
|
|
20
|
+
from ixt.libs.style import bold, cyan, dim, green, red, yellow
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ixt.core.apply import (
|
|
24
|
+
ApplyAction,
|
|
25
|
+
ApplyPlan,
|
|
26
|
+
ApplyResult,
|
|
27
|
+
InjectAction,
|
|
28
|
+
InstallAction,
|
|
29
|
+
ReexposeAction,
|
|
30
|
+
ReinstallAction,
|
|
31
|
+
UninjectAction,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cmd_apply(args: argparse.Namespace) -> int:
|
|
36
|
+
from ixt.core.apply import RemovalsRefused, apply_config
|
|
37
|
+
|
|
38
|
+
logger = get_logger("apply")
|
|
39
|
+
dry_run = getattr(args, "dry_run", False)
|
|
40
|
+
yes = getattr(args, "yes", False)
|
|
41
|
+
|
|
42
|
+
config_path = Path(args.config_file) if args.config_file else None
|
|
43
|
+
|
|
44
|
+
if dry_run:
|
|
45
|
+
return _cmd_apply_dry_run(config_path, args.remove, logger)
|
|
46
|
+
|
|
47
|
+
confirm: Callable[[list[str]], bool] | None = None
|
|
48
|
+
if args.remove and not yes:
|
|
49
|
+
confirm = _make_remove_confirmer(logger)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
start = time.monotonic()
|
|
53
|
+
result = apply_config(
|
|
54
|
+
config_path=config_path,
|
|
55
|
+
remove_unlisted=args.remove,
|
|
56
|
+
confirm_removals=confirm,
|
|
57
|
+
progress=_make_apply_progress(logger),
|
|
58
|
+
)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
logger.error("No ixt.toml found")
|
|
61
|
+
logger.error(f" {dim('Hint: create one with ixt tool export > ixt.toml')}")
|
|
62
|
+
return 1
|
|
63
|
+
except RemovalsRefused:
|
|
64
|
+
logger.info("aborted by user")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
total = len(result.installed) + len(result.updated) + len(result.removed)
|
|
68
|
+
if not total and not result.errors:
|
|
69
|
+
logger.info("Already in sync")
|
|
70
|
+
else:
|
|
71
|
+
logger.info(_apply_footer(result, time.monotonic() - start))
|
|
72
|
+
|
|
73
|
+
return 1 if result.errors else 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_apply_progress(logger):
|
|
77
|
+
"""Create a progress callback for apply_config."""
|
|
78
|
+
settings = get_settings()
|
|
79
|
+
started = False
|
|
80
|
+
|
|
81
|
+
def progress(
|
|
82
|
+
phase: str,
|
|
83
|
+
kind: str,
|
|
84
|
+
action: ApplyAction,
|
|
85
|
+
index: int,
|
|
86
|
+
total: int,
|
|
87
|
+
_result: ApplyResult,
|
|
88
|
+
error: Exception | None,
|
|
89
|
+
) -> None:
|
|
90
|
+
nonlocal started
|
|
91
|
+
prefix = dim(f"[{index}/{total}]")
|
|
92
|
+
if phase == "start":
|
|
93
|
+
if not started:
|
|
94
|
+
logger.info(f"applying {total} change(s)")
|
|
95
|
+
started = True
|
|
96
|
+
logger.info(_apply_progress_line(prefix, kind, action))
|
|
97
|
+
return
|
|
98
|
+
if phase == "done" and error is not None:
|
|
99
|
+
logger.error(f"{prefix} {red('x')} {_apply_action_name(action)}: {error}")
|
|
100
|
+
return
|
|
101
|
+
if phase == "done":
|
|
102
|
+
line = _apply_done_line(kind, action, settings)
|
|
103
|
+
if line is not None:
|
|
104
|
+
logger.info(line)
|
|
105
|
+
|
|
106
|
+
return progress
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _apply_action_name(action: ApplyAction) -> str:
|
|
110
|
+
if isinstance(action, str):
|
|
111
|
+
return action
|
|
112
|
+
return action.name
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _apply_progress_line(prefix: str, kind: str, action: ApplyAction) -> str:
|
|
116
|
+
name = _apply_action_name(action)
|
|
117
|
+
if kind == "install":
|
|
118
|
+
install = cast("InstallAction", action)
|
|
119
|
+
source = format_source_tag(
|
|
120
|
+
source_label_for_spec(
|
|
121
|
+
install.install_spec,
|
|
122
|
+
backend_hint=install.backend,
|
|
123
|
+
tool_id=install.name,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
source_suffix = f" {source}" if source else ""
|
|
127
|
+
display = bold(install.install_spec)
|
|
128
|
+
return f"{prefix} {dim('installing')} {display}{source_suffix}"
|
|
129
|
+
if kind == "reinstall":
|
|
130
|
+
reinstall = cast("ReinstallAction", action)
|
|
131
|
+
reasons = "+".join(reinstall.reasons)
|
|
132
|
+
source = format_source_tag(
|
|
133
|
+
source_label_for_spec(
|
|
134
|
+
reinstall.install_spec,
|
|
135
|
+
backend_hint=reinstall.backend,
|
|
136
|
+
tool_id=reinstall.name,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
source_suffix = f" {source}" if source else ""
|
|
140
|
+
return (
|
|
141
|
+
f"{prefix} {dim('reinstalling')} "
|
|
142
|
+
f"{bold(reinstall.install_spec)} {dim(reasons)}{source_suffix}"
|
|
143
|
+
)
|
|
144
|
+
if kind == "inject":
|
|
145
|
+
inject = cast("InjectAction", action)
|
|
146
|
+
packages = ", ".join(inject.packages)
|
|
147
|
+
return f"{prefix} {green('+')} {dim('injecting')} {bold(name)} {dim(packages)}"
|
|
148
|
+
if kind == "uninject":
|
|
149
|
+
uninject = cast("UninjectAction", action)
|
|
150
|
+
packages = ", ".join(uninject.packages)
|
|
151
|
+
return f"{prefix} {red('-')} {dim('uninjecting')} {bold(name)} {dim(packages)}"
|
|
152
|
+
if kind == "re-expose":
|
|
153
|
+
reexpose = cast("ReexposeAction", action)
|
|
154
|
+
old = ",".join(reexpose.old_rules) or "(none)"
|
|
155
|
+
new = ",".join(reexpose.new_rules) or "(none)"
|
|
156
|
+
return f"{prefix} {cyan('~')} {dim('re-exposing')} {bold(name)} {dim(old)} -> {dim(new)}"
|
|
157
|
+
if kind == "policy":
|
|
158
|
+
return f"{prefix} {cyan('~')} {dim('applying policy')} {bold(name)}"
|
|
159
|
+
return f"{prefix} {red('-')} {dim('removing')} {bold(name)}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _load_action_record(action: ApplyAction, settings) -> ToolRecord | None:
|
|
163
|
+
if isinstance(action, str):
|
|
164
|
+
return None
|
|
165
|
+
meta_file = settings.get_tool_metadata_file(action.name)
|
|
166
|
+
try:
|
|
167
|
+
return ToolRecord.load_json(meta_file)
|
|
168
|
+
except (FileNotFoundError, ValueError):
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _apply_record_done_line(status: str, record: ToolRecord) -> str:
|
|
173
|
+
version = dim(record.version or "unknown")
|
|
174
|
+
exposes = format_exposed_bins(record.exposed_bins)
|
|
175
|
+
marker = green("+") if status == "installed" else yellow("*")
|
|
176
|
+
return f" {marker} {status} {version} {dim('·')} exposes: {exposes}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _apply_done_line(kind: str, action: ApplyAction, settings) -> str | None:
|
|
180
|
+
if kind not in {"install", "reinstall", "re-expose"}:
|
|
181
|
+
return None
|
|
182
|
+
record = _load_action_record(action, settings)
|
|
183
|
+
if record is None:
|
|
184
|
+
return None
|
|
185
|
+
if kind == "install":
|
|
186
|
+
return _apply_record_done_line("installed", record)
|
|
187
|
+
if kind == "reinstall":
|
|
188
|
+
return _apply_record_done_line("reinstalled", record)
|
|
189
|
+
return (
|
|
190
|
+
f" {cyan('re-exposed')} {dim('·')} "
|
|
191
|
+
f"exposes: {format_exposed_bins(record.exposed_bins)}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _apply_footer(result: ApplyResult, elapsed: float) -> str:
|
|
196
|
+
labels = [
|
|
197
|
+
(len(result.installed), "installed"),
|
|
198
|
+
(len(result.updated), "updated"),
|
|
199
|
+
(len(result.removed), "removed"),
|
|
200
|
+
(len(result.errors), "failed"),
|
|
201
|
+
]
|
|
202
|
+
parts = [f"{count} {label}" for count, label in labels if count]
|
|
203
|
+
summary = ", ".join(parts) if parts else "nothing to do"
|
|
204
|
+
return f"{summary} {dim(f'in {elapsed:.1f}s')}"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _make_remove_confirmer(logger) -> Callable[[list[str]], bool]:
|
|
208
|
+
"""Build a confirmer that prompts y/N on TTY, refuses in non-interactive."""
|
|
209
|
+
import sys
|
|
210
|
+
|
|
211
|
+
def confirm(names: list[str]) -> bool:
|
|
212
|
+
summary = f"About to remove {len(names)} tool(s): {', '.join(sorted(names))}"
|
|
213
|
+
if not sys.stdin.isatty():
|
|
214
|
+
logger.error(summary)
|
|
215
|
+
logger.error(f" {dim('non-interactive session; pass --yes to confirm')}")
|
|
216
|
+
return False
|
|
217
|
+
print(summary, file=sys.stderr)
|
|
218
|
+
try:
|
|
219
|
+
answer = input("Proceed? [y/N] ").strip().lower()
|
|
220
|
+
except (EOFError, KeyboardInterrupt):
|
|
221
|
+
print("", file=sys.stderr)
|
|
222
|
+
return False
|
|
223
|
+
return answer in ("y", "yes")
|
|
224
|
+
|
|
225
|
+
return confirm
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _load_apply_plan(
|
|
229
|
+
config_path: Path | None, remove_unlisted: bool, logger
|
|
230
|
+
) -> tuple[ApplyPlan, Path] | int:
|
|
231
|
+
"""Load the ixt.toml and build an ApplyPlan, or return an exit code on error."""
|
|
232
|
+
from ixt.config.toml import find_config_file, load_config
|
|
233
|
+
from ixt.core.apply import _get_installed_tools, _normalize_config_for_apply, plan_apply
|
|
234
|
+
|
|
235
|
+
path = config_path or find_config_file()
|
|
236
|
+
if path is None:
|
|
237
|
+
logger.error("No ixt.toml found")
|
|
238
|
+
logger.error(f" {dim('Hint: create one with ixt tool export > ixt.toml')}")
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
config = load_config(path)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.error(f"Failed to load {path}: {exc}")
|
|
245
|
+
return 1
|
|
246
|
+
|
|
247
|
+
settings = get_settings()
|
|
248
|
+
installed = _get_installed_tools(settings)
|
|
249
|
+
config = _normalize_config_for_apply(config, settings)
|
|
250
|
+
plan = plan_apply(config, installed, remove_unlisted=remove_unlisted)
|
|
251
|
+
return plan, path
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _print_apply_installs(actions, logger) -> None:
|
|
255
|
+
for action in actions:
|
|
256
|
+
source = format_source_tag(
|
|
257
|
+
source_label_for_spec(
|
|
258
|
+
action.install_spec,
|
|
259
|
+
backend_hint=action.backend,
|
|
260
|
+
tool_id=action.name,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
source_suffix = f" {source}" if source else ""
|
|
264
|
+
display = bold(action.install_spec)
|
|
265
|
+
logger.info(f" {green('+')} {dim('would install')} {display}{source_suffix}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _print_apply_reinstalls(actions, logger) -> None:
|
|
269
|
+
for action in actions:
|
|
270
|
+
reason_str = "+".join(action.reasons)
|
|
271
|
+
source = format_source_tag(
|
|
272
|
+
source_label_for_spec(
|
|
273
|
+
action.install_spec,
|
|
274
|
+
backend_hint=action.backend,
|
|
275
|
+
tool_id=action.name,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
source_suffix = f" {source}" if source else ""
|
|
279
|
+
logger.info(
|
|
280
|
+
f" {yellow('*')} {dim('would reinstall')} {bold(action.install_spec)} "
|
|
281
|
+
f"{dim(f'(reinstall: {reason_str})')}{source_suffix}"
|
|
282
|
+
)
|
|
283
|
+
if "version" in action.reasons:
|
|
284
|
+
old = action.old_version or "?"
|
|
285
|
+
new = action.pinned_version or "?"
|
|
286
|
+
logger.info(f" {dim('version:')} {old} {dim('->')} {new}")
|
|
287
|
+
if "inject" in action.reasons:
|
|
288
|
+
added = sorted(set(action.new_inject) - set(action.old_inject))
|
|
289
|
+
removed = sorted(set(action.old_inject) - set(action.new_inject))
|
|
290
|
+
if added:
|
|
291
|
+
logger.info(f" {dim('inject +:')} {', '.join(added)}")
|
|
292
|
+
if removed:
|
|
293
|
+
logger.info(f" {dim('inject -:')} {', '.join(removed)}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _print_apply_injects(actions, logger) -> None:
|
|
297
|
+
for action in actions:
|
|
298
|
+
logger.info(
|
|
299
|
+
f" {green('+')} {dim('would inject')} {bold(action.name)} {dim('(inject:')} "
|
|
300
|
+
f"{', '.join(action.packages)}{dim(')')}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _print_apply_uninjects(actions, logger) -> None:
|
|
305
|
+
for action in actions:
|
|
306
|
+
logger.info(
|
|
307
|
+
f" {red('-')} {dim('would uninject')} {bold(action.name)} {dim('(uninject:')} "
|
|
308
|
+
f"{', '.join(action.packages)}{dim(')')}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _print_apply_reexposes(actions, logger) -> None:
|
|
313
|
+
for action in actions:
|
|
314
|
+
old = ",".join(action.old_rules) or "(none)"
|
|
315
|
+
new = ",".join(action.new_rules) or "(none)"
|
|
316
|
+
logger.info(
|
|
317
|
+
f" {cyan('~')} {dim('would re-expose')} {bold(action.name)} "
|
|
318
|
+
f"{dim('(re-expose:')} {old} "
|
|
319
|
+
f"{dim('->')} {new}{dim(')')}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _print_apply_repolicies(actions, logger) -> None:
|
|
324
|
+
for action in actions:
|
|
325
|
+
logger.info(f" {cyan('~')} {dim('would apply policy')} {bold(action.name)}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _print_apply_removes(names, logger) -> None:
|
|
329
|
+
for name in names:
|
|
330
|
+
logger.info(f" {red('-')} {dim('would remove')} {bold(name)}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _print_apply_summary(plan, logger) -> None:
|
|
334
|
+
"""Print the ``total: N install, M reinstall, ...`` summary line."""
|
|
335
|
+
sections: list[tuple[list, str]] = [
|
|
336
|
+
(plan.to_install, "install"),
|
|
337
|
+
(plan.to_reinstall, "reinstall"),
|
|
338
|
+
(plan.to_inject, "inject"),
|
|
339
|
+
(plan.to_uninject, "uninject"),
|
|
340
|
+
(plan.to_reexpose, "re-expose"),
|
|
341
|
+
(plan.to_repolicy, "policy"),
|
|
342
|
+
(plan.to_remove, "remove"),
|
|
343
|
+
]
|
|
344
|
+
parts = [f"{len(items)} {label}" for items, label in sections if items]
|
|
345
|
+
if parts:
|
|
346
|
+
logger.info(f" {dim('total: ' + ', '.join(parts))}")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _cmd_apply_dry_run(config_path: Path | None, remove_unlisted: bool, logger) -> int:
|
|
350
|
+
result = _load_apply_plan(config_path, remove_unlisted, logger)
|
|
351
|
+
if isinstance(result, int):
|
|
352
|
+
return result
|
|
353
|
+
plan, path = result
|
|
354
|
+
|
|
355
|
+
logger.info(f"{dim('dry-run: no changes will be applied')}")
|
|
356
|
+
logger.info(f"{dim('config: ')}{path}")
|
|
357
|
+
|
|
358
|
+
if plan.is_empty:
|
|
359
|
+
logger.info("Already in sync")
|
|
360
|
+
return 0
|
|
361
|
+
|
|
362
|
+
_print_apply_installs(plan.to_install, logger)
|
|
363
|
+
_print_apply_reinstalls(plan.to_reinstall, logger)
|
|
364
|
+
_print_apply_injects(plan.to_inject, logger)
|
|
365
|
+
_print_apply_uninjects(plan.to_uninject, logger)
|
|
366
|
+
_print_apply_reexposes(plan.to_reexpose, logger)
|
|
367
|
+
_print_apply_repolicies(plan.to_repolicy, logger)
|
|
368
|
+
_print_apply_removes(plan.to_remove, logger)
|
|
369
|
+
_print_apply_summary(plan, logger)
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def cmd_export(args: argparse.Namespace) -> int:
|
|
374
|
+
action = getattr(args, "export_action", None)
|
|
375
|
+
if action == "asset-index":
|
|
376
|
+
import sys
|
|
377
|
+
|
|
378
|
+
from ixt.core.export import export_asset_index_json
|
|
379
|
+
|
|
380
|
+
from_registry = getattr(args, "from_registry", None)
|
|
381
|
+
json_text = export_asset_index_json(
|
|
382
|
+
from_registry=Path(from_registry) if from_registry else None,
|
|
383
|
+
all_platforms=bool(getattr(args, "all_platforms", False)),
|
|
384
|
+
warn=lambda message: print(message, file=sys.stderr),
|
|
385
|
+
)
|
|
386
|
+
data(json_text.rstrip("\n"))
|
|
387
|
+
return 0
|
|
388
|
+
if action != "tools":
|
|
389
|
+
get_logger("export").error("Specify what to export: tools or asset-index")
|
|
390
|
+
return 1
|
|
391
|
+
|
|
392
|
+
# lazy: keeps `ixt.core.export` (and its TOML serializer chain)
|
|
393
|
+
# off the import path of every other CLI command.
|
|
394
|
+
from ixt.core.export import ToolNotInstalledError, export_toml
|
|
395
|
+
|
|
396
|
+
names = list(getattr(args, "names", []) or [])
|
|
397
|
+
try:
|
|
398
|
+
toml_text = export_toml(names or None)
|
|
399
|
+
except (ToolNotInstalledError, ValueError) as exc:
|
|
400
|
+
get_logger("export").error(str(exc))
|
|
401
|
+
return 1
|
|
402
|
+
|
|
403
|
+
data(toml_text.rstrip("\n"))
|
|
404
|
+
return 0
|
ixt/cli/cmd_cache.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Cache management subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ixt.config.settings import get_settings
|
|
8
|
+
from ixt.core.cache import cache_sizes, clear_caches, humanize_size, prune_downloads
|
|
9
|
+
from ixt.libs.logger import get_logger
|
|
10
|
+
from ixt.libs.style import bold, cyan, dim, green
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_cache(args: argparse.Namespace) -> int:
|
|
14
|
+
"""Dispatch ``ixt cache <subcommand>``."""
|
|
15
|
+
action = getattr(args, "cache_action", None)
|
|
16
|
+
if action == "info":
|
|
17
|
+
return _cmd_cache_info(args)
|
|
18
|
+
if action == "clear":
|
|
19
|
+
return _cmd_cache_clear(args)
|
|
20
|
+
if action == "prune":
|
|
21
|
+
return _cmd_cache_prune(args)
|
|
22
|
+
|
|
23
|
+
logger = get_logger("cache")
|
|
24
|
+
logger.error("Specify a cache subcommand (info, prune, clear)")
|
|
25
|
+
return 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cmd_cache_info(args: argparse.Namespace) -> int:
|
|
29
|
+
_ = args
|
|
30
|
+
logger = get_logger("cache")
|
|
31
|
+
settings = get_settings()
|
|
32
|
+
entries = cache_sizes(settings=settings)
|
|
33
|
+
total = sum(entry.bytes for entry in entries)
|
|
34
|
+
|
|
35
|
+
logger.info(f" {bold('ixt cache')}")
|
|
36
|
+
logger.info(f" Root: {cyan(str(settings.cache_home))}")
|
|
37
|
+
logger.info("")
|
|
38
|
+
for entry in entries:
|
|
39
|
+
files = _files(entry.files)
|
|
40
|
+
logger.info(
|
|
41
|
+
f" {entry.name:10s} {humanize_size(entry.bytes):>8s} "
|
|
42
|
+
f"{entry.files} {files} {dim(str(entry.path))}"
|
|
43
|
+
)
|
|
44
|
+
logger.info("")
|
|
45
|
+
logger.info(f" Total: {bold(humanize_size(total))}")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_cache_clear(args: argparse.Namespace) -> int:
|
|
50
|
+
logger = get_logger("cache")
|
|
51
|
+
for result in clear_caches(args.target):
|
|
52
|
+
files = _files(result.removed_count)
|
|
53
|
+
logger.info(
|
|
54
|
+
f" {green('ok')} cleared {result.target}: "
|
|
55
|
+
f"{humanize_size(result.removed_bytes)}, "
|
|
56
|
+
f"{result.removed_count} {files} ({dim(str(result.path))})"
|
|
57
|
+
)
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _cmd_cache_prune(args: argparse.Namespace) -> int:
|
|
62
|
+
logger = get_logger("cache")
|
|
63
|
+
try:
|
|
64
|
+
result = prune_downloads(keep=args.keep)
|
|
65
|
+
except ValueError as exc:
|
|
66
|
+
logger.error(str(exc))
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
files = _files(result.removed_count)
|
|
70
|
+
stale = (
|
|
71
|
+
f", dropped {result.stale_entries} stale metadata "
|
|
72
|
+
f"{'entry' if result.stale_entries == 1 else 'entries'}"
|
|
73
|
+
if result.stale_entries
|
|
74
|
+
else ""
|
|
75
|
+
)
|
|
76
|
+
logger.info(
|
|
77
|
+
f" {green('ok')} pruned downloads: "
|
|
78
|
+
f"removed {humanize_size(result.removed_bytes)}, "
|
|
79
|
+
f"{result.removed_count} {files}; kept {result.kept_count} indexed artifacts"
|
|
80
|
+
f"{stale}"
|
|
81
|
+
)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _files(count: int) -> str:
|
|
86
|
+
return "file" if count == 1 else "files"
|