ixt-cli 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. ixt/__init__.py +8 -0
  2. ixt/__main__.py +8 -0
  3. ixt/backends/__init__.py +1 -0
  4. ixt/backends/binary.py +935 -0
  5. ixt/backends/binary_resolver.py +307 -0
  6. ixt/backends/node.py +490 -0
  7. ixt/backends/python.py +234 -0
  8. ixt/cli/__init__.py +31 -0
  9. ixt/cli/argparse_completion.py +557 -0
  10. ixt/cli/cmd_apply.py +404 -0
  11. ixt/cli/cmd_cache.py +86 -0
  12. ixt/cli/cmd_config.py +295 -0
  13. ixt/cli/cmd_info.py +116 -0
  14. ixt/cli/cmd_install.py +508 -0
  15. ixt/cli/cmd_misc.py +261 -0
  16. ixt/cli/cmd_registry.py +35 -0
  17. ixt/cli/cmd_upgrade.py +336 -0
  18. ixt/cli/commands.py +70 -0
  19. ixt/cli/parser.py +555 -0
  20. ixt/cli/render.py +85 -0
  21. ixt/config/__init__.py +5 -0
  22. ixt/config/asset_index.py +305 -0
  23. ixt/config/asset_pattern_cache.py +87 -0
  24. ixt/config/env_policy.py +340 -0
  25. ixt/config/flags.py +29 -0
  26. ixt/config/fs_policy.py +17 -0
  27. ixt/config/heuristics.py +465 -0
  28. ixt/config/models.py +176 -0
  29. ixt/config/registry.py +145 -0
  30. ixt/config/settings.py +173 -0
  31. ixt/config/setup_toml.py +179 -0
  32. ixt/config/toml.py +416 -0
  33. ixt/core/__init__.py +16 -0
  34. ixt/core/apply.py +564 -0
  35. ixt/core/apply_actions.py +106 -0
  36. ixt/core/backend.py +187 -0
  37. ixt/core/bootstrap.py +410 -0
  38. ixt/core/cache.py +332 -0
  39. ixt/core/discover.py +150 -0
  40. ixt/core/doctor.py +591 -0
  41. ixt/core/export.py +419 -0
  42. ixt/core/expose.py +350 -0
  43. ixt/core/extract.py +261 -0
  44. ixt/core/hooks.py +182 -0
  45. ixt/core/identity.py +148 -0
  46. ixt/core/inject.py +143 -0
  47. ixt/core/install.py +509 -0
  48. ixt/core/install_local.py +229 -0
  49. ixt/core/locks.py +54 -0
  50. ixt/core/pathlink.py +86 -0
  51. ixt/core/resolution_stats.py +191 -0
  52. ixt/core/resolve.py +150 -0
  53. ixt/core/resolve_cache.py +185 -0
  54. ixt/core/runtimes.py +192 -0
  55. ixt/core/save.py +237 -0
  56. ixt/core/setup_completions.py +11 -0
  57. ixt/core/setup_path.py +368 -0
  58. ixt/core/upgrade.py +596 -0
  59. ixt/data/__init__.py +10 -0
  60. ixt/data/asset_index.json +574 -0
  61. ixt/data/heuristics.toml +98 -0
  62. ixt/data/registry.toml +71 -0
  63. ixt/libs/__init__.py +3 -0
  64. ixt/libs/constants.py +4 -0
  65. ixt/libs/fmt.py +108 -0
  66. ixt/libs/logger.py +109 -0
  67. ixt/libs/output.py +25 -0
  68. ixt/libs/req_spec.py +115 -0
  69. ixt/libs/semver.py +149 -0
  70. ixt/libs/shell.py +126 -0
  71. ixt/libs/style.py +238 -0
  72. ixt/net/__init__.py +1 -0
  73. ixt/net/github_api.py +158 -0
  74. ixt/net/gitlab_api.py +149 -0
  75. ixt/net/http.py +194 -0
  76. ixt/net/npm.py +24 -0
  77. ixt/net/pypi.py +26 -0
  78. ixt/net/source.py +163 -0
  79. ixt/platform/__init__.py +131 -0
  80. ixt/platform/win.py +68 -0
  81. ixt_cli-0.8.0.dist-info/METADATA +294 -0
  82. ixt_cli-0.8.0.dist-info/RECORD +84 -0
  83. ixt_cli-0.8.0.dist-info/WHEEL +4 -0
  84. ixt_cli-0.8.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,557 @@
1
+ """Shell completion generation from an ``argparse`` parser.
2
+
3
+ The parser remains the source of truth for commands, options, and choices.
4
+ This module extracts the small subset of argparse metadata needed to render
5
+ shell scripts without adding a third-party dependency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ from dataclasses import dataclass, field
12
+ from typing import Literal
13
+
14
+ SUPPORTED_COMPLETION_SHELLS = ("bash", "zsh", "fish")
15
+ CompletionValueKind = Literal["none", "file", "dir"]
16
+
17
+
18
+ def complete_with(action: argparse.Action, value_kind: CompletionValueKind) -> argparse.Action:
19
+ """Annotate an argparse action with a shell completion value kind."""
20
+ action.__dict__["_ixt_completion_value_kind"] = value_kind
21
+ return action
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CompletionArgument:
26
+ """One positional argument exposed by an argparse command."""
27
+
28
+ name: str
29
+ help: str = ""
30
+ choices: tuple[str, ...] = ()
31
+ value_kind: CompletionValueKind = "none"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class CompletionOption:
36
+ """One option exposed by an argparse command."""
37
+
38
+ flags: tuple[str, ...]
39
+ help: str = ""
40
+ choices: tuple[str, ...] = ()
41
+ takes_value: bool = False
42
+ value_kind: CompletionValueKind = "none"
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class CompletionCommand:
47
+ """A command node extracted from argparse."""
48
+
49
+ path: tuple[str, ...] = ()
50
+ help: str = ""
51
+ options: tuple[CompletionOption, ...] = ()
52
+ arguments: tuple[CompletionArgument, ...] = ()
53
+ subcommands: tuple[CompletionCommand, ...] = field(default_factory=tuple)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class CompletionCandidate:
58
+ """One word that can be completed, optionally with a description."""
59
+
60
+ word: str
61
+ help: str = ""
62
+
63
+
64
+ def render_completion(parser: argparse.ArgumentParser, shell: str) -> str:
65
+ """Render a completion script for *shell* from *parser*."""
66
+ tree = extract_completion_tree(parser)
67
+ if shell == "bash":
68
+ return _render_bash(tree)
69
+ if shell == "zsh":
70
+ return _render_zsh(tree)
71
+ if shell == "fish":
72
+ return _render_fish(tree)
73
+ supported = ", ".join(SUPPORTED_COMPLETION_SHELLS)
74
+ raise ValueError(f"unsupported completion shell: {shell} (expected one of: {supported})")
75
+
76
+
77
+ def extract_completion_tree(parser: argparse.ArgumentParser) -> CompletionCommand:
78
+ """Extract command, option, and choice metadata from *parser*."""
79
+ return _extract_command(parser, path=(), help_text=parser.description or "")
80
+
81
+
82
+ def _extract_command(
83
+ parser: argparse.ArgumentParser,
84
+ *,
85
+ path: tuple[str, ...],
86
+ help_text: str = "",
87
+ ) -> CompletionCommand:
88
+ options: list[CompletionOption] = []
89
+ arguments: list[CompletionArgument] = []
90
+ subcommands: list[CompletionCommand] = []
91
+
92
+ for action in parser._actions:
93
+ if _is_subparsers_action(action):
94
+ helps = {
95
+ choice.dest: choice.help or "" for choice in getattr(action, "_choices_actions", [])
96
+ }
97
+ hidden = {
98
+ str(choice.dest)
99
+ for choice in getattr(action, "_choices_actions", [])
100
+ if choice.help == argparse.SUPPRESS
101
+ }
102
+ choices = getattr(action, "choices", None)
103
+ if not isinstance(choices, dict):
104
+ continue
105
+ for name, subparser in choices.items():
106
+ if not isinstance(subparser, argparse.ArgumentParser):
107
+ continue
108
+ if str(name) in hidden:
109
+ continue
110
+ subcommands.append(
111
+ _extract_command(
112
+ subparser,
113
+ path=(*path, str(name)),
114
+ help_text=helps.get(str(name), subparser.description or ""),
115
+ )
116
+ )
117
+ continue
118
+
119
+ if action.help == argparse.SUPPRESS:
120
+ continue
121
+
122
+ if action.option_strings:
123
+ options.append(
124
+ CompletionOption(
125
+ flags=tuple(action.option_strings),
126
+ help=action.help or "",
127
+ choices=_choices(action),
128
+ takes_value=getattr(action, "nargs", None) != 0,
129
+ value_kind=_value_kind(action),
130
+ )
131
+ )
132
+ else:
133
+ arguments.append(
134
+ CompletionArgument(
135
+ name=action.dest,
136
+ help=action.help or "",
137
+ choices=_choices(action),
138
+ value_kind=_value_kind(action),
139
+ )
140
+ )
141
+
142
+ return CompletionCommand(
143
+ path=path,
144
+ help=help_text,
145
+ options=tuple(options),
146
+ arguments=tuple(arguments),
147
+ subcommands=tuple(subcommands),
148
+ )
149
+
150
+
151
+ def _is_subparsers_action(action: argparse.Action) -> bool:
152
+ return action.__class__.__name__ == "_SubParsersAction"
153
+
154
+
155
+ def _choices(action: argparse.Action) -> tuple[str, ...]:
156
+ choices = getattr(action, "choices", None)
157
+ if choices is None or isinstance(choices, dict):
158
+ return ()
159
+ return tuple(str(choice) for choice in choices)
160
+
161
+
162
+ def _value_kind(action: argparse.Action) -> CompletionValueKind:
163
+ value = getattr(action, "_ixt_completion_value_kind", "none")
164
+ if value in ("none", "file", "dir"):
165
+ return value
166
+ return "none"
167
+
168
+
169
+ def _iter_commands(command: CompletionCommand) -> list[CompletionCommand]:
170
+ commands = [command]
171
+ for child in command.subcommands:
172
+ commands.extend(_iter_commands(child))
173
+ return commands
174
+
175
+
176
+ def _path_key(path: tuple[str, ...]) -> str:
177
+ return " ".join(path)
178
+
179
+
180
+ def _command_candidates(command: CompletionCommand) -> list[str]:
181
+ return [candidate.word for candidate in _command_candidate_details(command)]
182
+
183
+
184
+ def _command_candidate_details(command: CompletionCommand) -> list[CompletionCandidate]:
185
+ words: list[CompletionCandidate] = []
186
+ words.extend(CompletionCandidate(child.path[-1], child.help) for child in command.subcommands)
187
+ for option in command.options:
188
+ words.extend(CompletionCandidate(flag, option.help) for flag in option.flags)
189
+ return words
190
+
191
+
192
+ def _argument_value_completion(command: CompletionCommand) -> CompletionArgument | None:
193
+ if command.subcommands:
194
+ return None
195
+ for argument in command.arguments:
196
+ if argument.choices or argument.value_kind != "none":
197
+ return argument
198
+ return None
199
+
200
+
201
+ def _word_list(words: list[str] | tuple[str, ...]) -> str:
202
+ return " ".join(words)
203
+
204
+
205
+ def _zsh_quote(value: str) -> str:
206
+ return (
207
+ '"'
208
+ + (value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("`", "\\`"))
209
+ + '"'
210
+ )
211
+
212
+
213
+ def _zsh_candidate(value: str, help_text: str = "") -> str:
214
+ return _zsh_quote(f"{value}:{help_text}" if help_text else value)
215
+
216
+
217
+ def _case_label(value: str) -> str:
218
+ return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
219
+
220
+
221
+ def _bash_value_completion_lines(
222
+ *,
223
+ value_kind: CompletionValueKind,
224
+ choices: tuple[str, ...] = (),
225
+ ) -> list[str]:
226
+ if choices:
227
+ return [
228
+ f' COMPREPLY=( $(compgen -W "{_word_list(choices)}" -- "$cur") )',
229
+ " return 0",
230
+ ]
231
+ if value_kind == "file":
232
+ return [
233
+ ' COMPREPLY=( $(compgen -f -- "$cur") )',
234
+ " return 0",
235
+ ]
236
+ if value_kind == "dir":
237
+ return [
238
+ ' COMPREPLY=( $(compgen -d -- "$cur") )',
239
+ " return 0",
240
+ ]
241
+ return [" return 0"]
242
+
243
+
244
+ def _zsh_value_completion_lines(
245
+ *,
246
+ value_kind: CompletionValueKind,
247
+ choices: tuple[str, ...] = (),
248
+ ) -> list[str]:
249
+ if choices:
250
+ return [
251
+ f" compadd -- {_word_list(choices)}",
252
+ " return",
253
+ ]
254
+ if value_kind == "file":
255
+ return [
256
+ " _files",
257
+ " return",
258
+ ]
259
+ if value_kind == "dir":
260
+ return [
261
+ " _files -/",
262
+ " return",
263
+ ]
264
+ return [" return"]
265
+
266
+
267
+ def _bash_path_detector(root: CompletionCommand) -> list[str]:
268
+ lines = [
269
+ ' local path=""',
270
+ " local word",
271
+ " local idx=1",
272
+ " while [[ $idx -lt $COMP_CWORD ]]; do",
273
+ ' word="${COMP_WORDS[$idx]}"',
274
+ ' case "$path" in',
275
+ ]
276
+ for command in _iter_commands(root):
277
+ if not command.subcommands:
278
+ continue
279
+ key = _path_key(command.path)
280
+ patterns = "|".join(child.path[-1] for child in command.subcommands)
281
+ next_path = "$word" if not key else f"{key} $word"
282
+ lines.extend(
283
+ [
284
+ f" {_case_label(key)})",
285
+ ' case "$word" in',
286
+ f' {patterns}) path="{next_path}" ;;',
287
+ " esac",
288
+ " ;;",
289
+ ]
290
+ )
291
+ lines.extend(
292
+ [
293
+ " esac",
294
+ " ((idx++))",
295
+ " done",
296
+ ]
297
+ )
298
+ return lines
299
+
300
+
301
+ def _zsh_path_detector(root: CompletionCommand) -> list[str]:
302
+ lines = [
303
+ ' local path=""',
304
+ " local word",
305
+ " local idx=2",
306
+ " while (( idx < CURRENT )); do",
307
+ ' word="${words[idx]}"',
308
+ ' case "$path" in',
309
+ ]
310
+ for command in _iter_commands(root):
311
+ if not command.subcommands:
312
+ continue
313
+ key = _path_key(command.path)
314
+ patterns = "|".join(child.path[-1] for child in command.subcommands)
315
+ next_path = "$word" if not key else f"{key} $word"
316
+ lines.extend(
317
+ [
318
+ f" {_case_label(key)})",
319
+ ' case "$word" in',
320
+ f' {patterns}) path="{next_path}" ;;',
321
+ " esac",
322
+ " ;;",
323
+ ]
324
+ )
325
+ lines.extend(
326
+ [
327
+ " esac",
328
+ " (( idx++ ))",
329
+ " done",
330
+ ]
331
+ )
332
+ return lines
333
+
334
+
335
+ def _render_bash(root: CompletionCommand) -> str:
336
+ lines = [
337
+ "# bash completion for ixt",
338
+ "_ixt_completion() {",
339
+ " local cur prev",
340
+ " COMPREPLY=()",
341
+ ' cur="${COMP_WORDS[COMP_CWORD]}"',
342
+ ' prev="${COMP_WORDS[COMP_CWORD-1]}"',
343
+ "",
344
+ ]
345
+ lines.extend(_bash_path_detector(root))
346
+ lines.extend(["", ' case "$path|$prev" in'])
347
+ for command in _iter_commands(root):
348
+ key = _path_key(command.path)
349
+ for option in command.options:
350
+ if not option.takes_value:
351
+ continue
352
+ for flag in option.flags:
353
+ lines.extend(
354
+ [
355
+ f" {_case_label(f'{key}|{flag}')})",
356
+ *_bash_value_completion_lines(
357
+ choices=option.choices,
358
+ value_kind=option.value_kind,
359
+ ),
360
+ " ;;",
361
+ ]
362
+ )
363
+ lines.extend([" esac", "", ' case "$path" in'])
364
+ for command in _iter_commands(root):
365
+ candidates = _command_candidates(command)
366
+ argument = _argument_value_completion(command)
367
+ if not candidates and argument is None:
368
+ continue
369
+ key = _path_key(command.path)
370
+ words = _word_list(candidates)
371
+ lines.append(f" {_case_label(key)})")
372
+ if words:
373
+ lines.append(f' COMPREPLY=( $(compgen -W "{words}" -- "$cur") )')
374
+ if argument is not None:
375
+ if argument.choices:
376
+ lines.append(
377
+ f' COMPREPLY+=( $(compgen -W "{_word_list(argument.choices)}" -- "$cur") )'
378
+ )
379
+ elif argument.value_kind == "file":
380
+ lines.append(' COMPREPLY+=( $(compgen -f -- "$cur") )')
381
+ elif argument.value_kind == "dir":
382
+ lines.append(' COMPREPLY+=( $(compgen -d -- "$cur") )')
383
+ lines.append(" ;;")
384
+ lines.extend(
385
+ [
386
+ " esac",
387
+ " return 0",
388
+ "}",
389
+ "complete -F _ixt_completion ixt",
390
+ "",
391
+ ]
392
+ )
393
+ return "\n".join(lines)
394
+
395
+
396
+ def _render_zsh(root: CompletionCommand) -> str:
397
+ lines = [
398
+ "#compdef ixt",
399
+ "",
400
+ "_ixt() {",
401
+ " local cur prev",
402
+ ' cur="${words[CURRENT]}"',
403
+ ' prev="${words[CURRENT-1]}"',
404
+ "",
405
+ ]
406
+ lines.extend(_zsh_path_detector(root))
407
+ lines.extend(["", ' case "$path|$prev" in'])
408
+ for command in _iter_commands(root):
409
+ key = _path_key(command.path)
410
+ for option in command.options:
411
+ if not option.takes_value:
412
+ continue
413
+ for flag in option.flags:
414
+ lines.extend(
415
+ [
416
+ f" {_case_label(f'{key}|{flag}')})",
417
+ *_zsh_value_completion_lines(
418
+ choices=option.choices,
419
+ value_kind=option.value_kind,
420
+ ),
421
+ " ;;",
422
+ ]
423
+ )
424
+ lines.extend([" esac", "", ' case "$path" in'])
425
+ for command in _iter_commands(root):
426
+ candidates = _command_candidate_details(command)
427
+ argument = _argument_value_completion(command)
428
+ if not candidates and argument is None:
429
+ continue
430
+ key = _path_key(command.path)
431
+ lines.append(f" {_case_label(key)})")
432
+ if candidates:
433
+ lines.append(" local -a candidates")
434
+ lines.append(" candidates=(")
435
+ for candidate in candidates:
436
+ lines.append(f" {_zsh_candidate(candidate.word, candidate.help)}")
437
+ lines.append(" )")
438
+ lines.append(" _describe 'ixt completions' candidates")
439
+ if argument is not None:
440
+ lines.extend(
441
+ " " + line
442
+ for line in _zsh_value_completion_lines(
443
+ choices=argument.choices,
444
+ value_kind=argument.value_kind,
445
+ )
446
+ if "return" not in line
447
+ )
448
+ lines.append(" ;;")
449
+ lines.extend(
450
+ [
451
+ " esac",
452
+ "}",
453
+ "",
454
+ "compdef _ixt ixt",
455
+ "",
456
+ ]
457
+ )
458
+ return "\n".join(lines)
459
+
460
+
461
+ def _fish_quote(value: str) -> str:
462
+ return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
463
+
464
+
465
+ def _fish_condition_for_path(path: tuple[str, ...]) -> str:
466
+ if not path:
467
+ return ""
468
+ return "; and ".join(f"__fish_seen_subcommand_from {part}" for part in path)
469
+
470
+
471
+ def _fish_condition_for_child(parent: CompletionCommand) -> str:
472
+ if not parent.path:
473
+ return "__fish_use_subcommand"
474
+ pieces = [_fish_condition_for_path(parent.path)]
475
+ if parent.subcommands:
476
+ siblings = " ".join(child.path[-1] for child in parent.subcommands)
477
+ pieces.append(f"not __fish_seen_subcommand_from {siblings}")
478
+ return "; and ".join(piece for piece in pieces if piece)
479
+
480
+
481
+ def _fish_option_names(option: CompletionOption) -> list[str]:
482
+ parts: list[str] = []
483
+ long_flags = [flag for flag in option.flags if flag.startswith("--")]
484
+ short_flags = [
485
+ flag for flag in option.flags if flag.startswith("-") and not flag.startswith("--")
486
+ ]
487
+ if long_flags:
488
+ parts.extend(["-l", long_flags[0][2:]])
489
+ if short_flags:
490
+ parts.extend(["-s", short_flags[0][1:]])
491
+ return parts
492
+
493
+
494
+ def _fish_argument_value(argument: CompletionArgument) -> str | None:
495
+ if argument.choices:
496
+ return " ".join(argument.choices)
497
+ if argument.value_kind == "dir":
498
+ return "(__fish_complete_directories)"
499
+ if argument.value_kind == "file":
500
+ return "(__fish_complete_path)"
501
+ return None
502
+
503
+
504
+ def _render_fish(root: CompletionCommand) -> str:
505
+ lines = ["# fish completion for ixt"]
506
+ for command in _iter_commands(root):
507
+ condition = _fish_condition_for_path(command.path)
508
+ for option in command.options:
509
+ parts = ["complete", "-c", "ixt"]
510
+ if option.value_kind not in ("file", "dir"):
511
+ parts.append("-f")
512
+ if condition:
513
+ parts.extend(["-n", _fish_quote(condition)])
514
+ parts.extend(_fish_option_names(option))
515
+ if option.takes_value:
516
+ parts.append("-r")
517
+ if option.choices:
518
+ parts.extend(["-a", _fish_quote(" ".join(option.choices))])
519
+ elif option.value_kind == "dir":
520
+ parts.extend(["-a", _fish_quote("(__fish_complete_directories)")])
521
+ elif option.value_kind == "file":
522
+ parts.extend(["-a", _fish_quote("(__fish_complete_path)")])
523
+ if option.help:
524
+ parts.extend(["-d", _fish_quote(option.help)])
525
+ lines.append(" ".join(parts))
526
+
527
+ argument = _argument_value_completion(command)
528
+ argument_value = _fish_argument_value(argument) if argument else None
529
+ if argument is not None and argument_value is not None:
530
+ parts = ["complete", "-c", "ixt"]
531
+ if condition:
532
+ parts.extend(["-n", _fish_quote(condition)])
533
+ if argument.value_kind == "dir":
534
+ parts.append("-f")
535
+ parts.extend(["-a", _fish_quote(argument_value)])
536
+ if argument.help:
537
+ parts.extend(["-d", _fish_quote(argument.help)])
538
+ lines.append(" ".join(parts))
539
+
540
+ child_condition = _fish_condition_for_child(command)
541
+ for child in command.subcommands:
542
+ parts = [
543
+ "complete",
544
+ "-c",
545
+ "ixt",
546
+ "-f",
547
+ "-n",
548
+ _fish_quote(child_condition),
549
+ "-a",
550
+ _fish_quote(child.path[-1]),
551
+ ]
552
+ if child.help:
553
+ parts.extend(["-d", _fish_quote(child.help)])
554
+ lines.append(" ".join(parts))
555
+
556
+ lines.append("")
557
+ return "\n".join(lines)