gitwise-cli 0.24.2__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 (125) hide show
  1. gitwise/__init__.py +11 -0
  2. gitwise/__main__.py +113 -0
  3. gitwise/_cli_completions.py +88 -0
  4. gitwise/_cli_dispatch.py +469 -0
  5. gitwise/_cli_introspection.py +275 -0
  6. gitwise/_cli_parser.py +345 -0
  7. gitwise/_cli_setup_agents.py +439 -0
  8. gitwise/_i18n_data.json +1934 -0
  9. gitwise/_paths.py +22 -0
  10. gitwise/_runtime_config.py +246 -0
  11. gitwise/audit.py +338 -0
  12. gitwise/branches.py +183 -0
  13. gitwise/clean.py +197 -0
  14. gitwise/commit.py +142 -0
  15. gitwise/conflicts.py +112 -0
  16. gitwise/context.py +163 -0
  17. gitwise/design.py +383 -0
  18. gitwise/diff.py +309 -0
  19. gitwise/doctor.py +116 -0
  20. gitwise/git.py +254 -0
  21. gitwise/health.py +345 -0
  22. gitwise/i18n.py +99 -0
  23. gitwise/log.py +329 -0
  24. gitwise/merge.py +193 -0
  25. gitwise/optimize.py +212 -0
  26. gitwise/output.py +652 -0
  27. gitwise/pick.py +102 -0
  28. gitwise/pr.py +543 -0
  29. gitwise/py.typed +0 -0
  30. gitwise/schema.py +49 -0
  31. gitwise/setup.py +551 -0
  32. gitwise/setup_agents/__init__.py +36 -0
  33. gitwise/setup_agents/adapters/__init__.py +17 -0
  34. gitwise/setup_agents/adapters/aider.py +5 -0
  35. gitwise/setup_agents/adapters/base.py +5 -0
  36. gitwise/setup_agents/adapters/codex.py +5 -0
  37. gitwise/setup_agents/adapters/continue_adapter.py +5 -0
  38. gitwise/setup_agents/adapters/cursor.py +5 -0
  39. gitwise/setup_agents/adapters/opencode.py +5 -0
  40. gitwise/setup_agents/adapters/pi.py +5 -0
  41. gitwise/setup_agents/exec.py +449 -0
  42. gitwise/setup_agents/format.py +164 -0
  43. gitwise/setup_agents/plan.py +254 -0
  44. gitwise/setup_agents/plan_gitfiles.py +167 -0
  45. gitwise/setup_agents/plan_skills.py +256 -0
  46. gitwise/setup_agents/providers/__init__.py +96 -0
  47. gitwise/setup_agents/providers/aider.py +11 -0
  48. gitwise/setup_agents/providers/base.py +79 -0
  49. gitwise/setup_agents/providers/claude.py +408 -0
  50. gitwise/setup_agents/providers/codex.py +11 -0
  51. gitwise/setup_agents/providers/continue_adapter.py +11 -0
  52. gitwise/setup_agents/providers/cursor.py +11 -0
  53. gitwise/setup_agents/providers/opencode.py +11 -0
  54. gitwise/setup_agents/providers/pi.py +11 -0
  55. gitwise/setup_agents/state.py +141 -0
  56. gitwise/setup_agents/types.py +48 -0
  57. gitwise/share/agents/skills/git-audit/SKILL.md +25 -0
  58. gitwise/share/agents/skills/git-clean/SKILL.md +22 -0
  59. gitwise/share/agents/skills/git-optimize/SKILL.md +21 -0
  60. gitwise/share/aider/CONVENTIONS.md.template +8 -0
  61. gitwise/share/aider/aider.conf.yml.template +4 -0
  62. gitwise/share/claude/CLAUDE.md.template +9 -0
  63. gitwise/share/claude/rules/gitwise.md +16 -0
  64. gitwise/share/claude/settings.json.template +47 -0
  65. gitwise/share/claude/skills/git-audit/SKILL.md +25 -0
  66. gitwise/share/claude/skills/git-clean/SKILL.md +22 -0
  67. gitwise/share/claude/skills/git-optimize/SKILL.md +21 -0
  68. gitwise/share/codex/agents/gitwise.toml.template +18 -0
  69. gitwise/share/continue/rules/gitwise.md.template +14 -0
  70. gitwise/share/cursor/rules/gitwise.mdc.template +16 -0
  71. gitwise/share/git-config-modern.txt +48 -0
  72. gitwise/share/hooks/commit-msg +22 -0
  73. gitwise/share/hooks/pre-commit +19 -0
  74. gitwise/share/opencode/agents/gitwise.md.template +14 -0
  75. gitwise/share/pi/skills/gitwise.md.template +14 -0
  76. gitwise/share/schemas/v1/input/audit.json +40 -0
  77. gitwise/share/schemas/v1/input/branches.json +51 -0
  78. gitwise/share/schemas/v1/input/clean.json +52 -0
  79. gitwise/share/schemas/v1/input/commands.json +36 -0
  80. gitwise/share/schemas/v1/input/commit.json +63 -0
  81. gitwise/share/schemas/v1/input/completions.json +51 -0
  82. gitwise/share/schemas/v1/input/conflicts.json +46 -0
  83. gitwise/share/schemas/v1/input/context.json +36 -0
  84. gitwise/share/schemas/v1/input/diff.json +56 -0
  85. gitwise/share/schemas/v1/input/doctor.json +36 -0
  86. gitwise/share/schemas/v1/input/health.json +36 -0
  87. gitwise/share/schemas/v1/input/log.json +71 -0
  88. gitwise/share/schemas/v1/input/merge.json +63 -0
  89. gitwise/share/schemas/v1/input/optimize.json +44 -0
  90. gitwise/share/schemas/v1/input/pick.json +63 -0
  91. gitwise/share/schemas/v1/input/pr.json +51 -0
  92. gitwise/share/schemas/v1/input/schema.json +48 -0
  93. gitwise/share/schemas/v1/input/setup-agents.json +108 -0
  94. gitwise/share/schemas/v1/input/setup.json +55 -0
  95. gitwise/share/schemas/v1/input/show.json +46 -0
  96. gitwise/share/schemas/v1/input/snapshot.json +36 -0
  97. gitwise/share/schemas/v1/input/stash.json +68 -0
  98. gitwise/share/schemas/v1/input/status.json +36 -0
  99. gitwise/share/schemas/v1/input/suggest.json +36 -0
  100. gitwise/share/schemas/v1/input/summarize.json +44 -0
  101. gitwise/share/schemas/v1/input/sync.json +55 -0
  102. gitwise/share/schemas/v1/input/tag.json +73 -0
  103. gitwise/share/schemas/v1/input/undo.json +60 -0
  104. gitwise/share/schemas/v1/input/update.json +40 -0
  105. gitwise/share/schemas/v1/input/worktree.json +50 -0
  106. gitwise/show.py +118 -0
  107. gitwise/snapshot.py +110 -0
  108. gitwise/stash.py +188 -0
  109. gitwise/status.py +93 -0
  110. gitwise/suggest.py +148 -0
  111. gitwise/summarize.py +202 -0
  112. gitwise/sync.py +257 -0
  113. gitwise/tag.py +252 -0
  114. gitwise/undo.py +145 -0
  115. gitwise/update.py +42 -0
  116. gitwise/utils/__init__.py +1 -0
  117. gitwise/utils/git_output.py +51 -0
  118. gitwise/utils/json_envelope.py +58 -0
  119. gitwise/utils/parsing.py +34 -0
  120. gitwise/worktree.py +182 -0
  121. gitwise_cli-0.24.2.dist-info/METADATA +151 -0
  122. gitwise_cli-0.24.2.dist-info/RECORD +125 -0
  123. gitwise_cli-0.24.2.dist-info/WHEEL +4 -0
  124. gitwise_cli-0.24.2.dist-info/entry_points.txt +2 -0
  125. gitwise_cli-0.24.2.dist-info/licenses/LICENSE +21 -0
gitwise/output.py ADDED
@@ -0,0 +1,652 @@
1
+ """Output: Rich-powered human-readable + plain JSON. Detects bat/delta."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+ from typing import TYPE_CHECKING, Any, Literal
14
+
15
+ if TYPE_CHECKING:
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+ from rich.theme import Theme
20
+
21
+ try:
22
+ from rich.console import Console
23
+ from rich.table import Table
24
+ from rich.text import Text
25
+ from rich.theme import Theme
26
+
27
+ _HAS_RICH = True
28
+ except ImportError:
29
+ _HAS_RICH = False
30
+
31
+ from ._runtime_config import get_runtime_config
32
+ from .design import ColorDepth, pad_right, truncate, visible_length
33
+ from .i18n import confirm_responses, t
34
+
35
+ _COLOR_SYSTEM_MAP: dict[ColorDepth, str] = {
36
+ "truecolor": "truecolor",
37
+ "256color": "256",
38
+ "16color": "16",
39
+ }
40
+
41
+ _LOG_JSON = os.environ.get("GITWISE_LOG_JSON", "").lower() in ("1", "true")
42
+ _JSON_PRETTY = os.environ.get("GITWISE_JSON_PRETTY", "").lower() in ("1", "true")
43
+
44
+
45
+ def _structured_log(level: str, msg: str, **kwargs: Any) -> None:
46
+ entry: dict[str, Any] = {
47
+ "ts": time.time(),
48
+ "level": level,
49
+ "msg": msg,
50
+ }
51
+ if kwargs:
52
+ entry.update(kwargs)
53
+ sys.stderr.write(json.dumps(entry, default=str) + "\n")
54
+
55
+
56
+ def set_json_pretty(pretty: bool) -> None:
57
+ global _JSON_PRETTY
58
+ _JSON_PRETTY = pretty
59
+
60
+
61
+ class _ModuleAttr:
62
+ __slots__ = ("_name",)
63
+
64
+ def __init__(self, name: str) -> None:
65
+ self._name = name
66
+
67
+ def __bool__(self) -> bool:
68
+ return bool(getattr(get_runtime_config(), self._name))
69
+
70
+ def __repr__(self) -> str:
71
+ return repr(bool(self))
72
+
73
+
74
+ HAS_BAT = _ModuleAttr("has_bat")
75
+ HAS_DELTA = _ModuleAttr("has_delta")
76
+ IS_TTY = _ModuleAttr("is_tty")
77
+
78
+
79
+ def _build_rich_theme() -> dict[str, str]:
80
+ tokens = get_runtime_config().theme_tokens
81
+ return {
82
+ "fg": tokens.fg,
83
+ "secondary": tokens.secondary,
84
+ "dim": tokens.dim or tokens.secondary,
85
+ "accent": tokens.accent,
86
+ "success": tokens.success,
87
+ "error": tokens.error,
88
+ "warning": tokens.warning or tokens.accent,
89
+ "brand": tokens.brand or tokens.accent,
90
+ "bold": f"bold {tokens.fg}",
91
+ "header": f"bold {tokens.fg}",
92
+ "rule": tokens.dim or tokens.secondary,
93
+ "bold.accent": f"bold {tokens.accent}",
94
+ }
95
+
96
+
97
+ def _color_disabled() -> bool:
98
+ return os.environ.get("NO_COLOR", "") != "" or os.environ.get(
99
+ "GITWISE_NO_COLOR", ""
100
+ ).lower() in ("1", "true")
101
+
102
+
103
+ def _color_forced() -> bool:
104
+ return (
105
+ os.environ.get("CLICOLOR_FORCE", "").lower()
106
+ in (
107
+ "1",
108
+ "true",
109
+ )
110
+ or os.environ.get("FORCE_COLOR", "") != ""
111
+ )
112
+
113
+
114
+ def _use_rich() -> bool:
115
+ if not _HAS_RICH:
116
+ return False
117
+ if _color_disabled():
118
+ return False
119
+ if _color_forced():
120
+ return True
121
+ return get_runtime_config().is_tty
122
+
123
+
124
+ _use_rich_cached: bool | None = None
125
+
126
+
127
+ def _should_use_rich() -> bool:
128
+ global _use_rich_cached
129
+ if _use_rich_cached is None:
130
+ _use_rich_cached = _use_rich()
131
+ return _use_rich_cached
132
+
133
+
134
+ def _make_console(*, file: Any = sys.stdout, force: bool = False) -> Console:
135
+ cfg = get_runtime_config()
136
+ depth = cfg.color_depth
137
+ console = Console(
138
+ theme=Theme(_build_rich_theme()),
139
+ color_system=_COLOR_SYSTEM_MAP.get(depth, "auto"), # pyright: ignore[reportArgumentType]
140
+ no_color=None,
141
+ force_terminal=force,
142
+ width=cfg.terminal_width,
143
+ legacy_windows=False,
144
+ highlight=False,
145
+ emoji=False,
146
+ markup=False,
147
+ file=file,
148
+ )
149
+ if cfg.debug:
150
+ sys.stderr.write(
151
+ f"[gitwise debug] console: force_terminal={force}, "
152
+ f"color_system={console.color_system}, "
153
+ f"is_terminal={console.is_terminal}, "
154
+ f"no_color={console.no_color}, "
155
+ f"theme={cfg.theme}, depth={depth}, "
156
+ f"is_tty={cfg.is_tty}\n"
157
+ )
158
+ return console
159
+
160
+
161
+ _console: Console | None = None
162
+ _stderr_console: Console | None = None
163
+
164
+
165
+ def _get_console() -> Console:
166
+ global _console
167
+ if _console is None:
168
+ _console = _make_console(force=True)
169
+ return _console
170
+
171
+
172
+ def _get_stderr_console() -> Console:
173
+ global _stderr_console
174
+ if _stderr_console is None:
175
+ _stderr_console = _make_console(file=sys.stderr, force=True)
176
+ return _stderr_console
177
+
178
+
179
+ def info(msg: str) -> None:
180
+ if _should_use_rich():
181
+ _get_console().print(Text(msg, style="dim"))
182
+ else:
183
+ print(msg)
184
+
185
+
186
+ def warn(msg: str) -> None:
187
+ if _LOG_JSON:
188
+ _structured_log("warn", msg)
189
+ return
190
+ prefix = t("warning_label")
191
+ if _should_use_rich():
192
+ text = Text()
193
+ text.append(f"{prefix}: ", style="warning")
194
+ text.append(msg)
195
+ _get_stderr_console().print(text)
196
+ else:
197
+ print(f"{prefix}: {msg}", file=sys.stderr)
198
+
199
+
200
+ def error(msg: str, *, hint: str | None = None) -> None:
201
+ if _LOG_JSON:
202
+ if hint:
203
+ _structured_log("error", msg, hint=hint)
204
+ else:
205
+ _structured_log("error", msg)
206
+ return
207
+ prefix = t("error")
208
+ if _should_use_rich():
209
+ text = Text()
210
+ text.append(f"{prefix}: ", style="error")
211
+ text.append(msg)
212
+ _get_stderr_console().print(text)
213
+ if hint:
214
+ hint_text = Text()
215
+ hint_text.append(f"{t('hint_prefix')}: ", style="dim")
216
+ hint_text.append(hint, style="dim")
217
+ _get_stderr_console().print(hint_text)
218
+ else:
219
+ print(f"{prefix}: {msg}", file=sys.stderr)
220
+ if hint:
221
+ print(f"{t('hint_prefix')}: {hint}", file=sys.stderr)
222
+
223
+
224
+ def ok(msg: str) -> None:
225
+ prefix = t("ok_prefix")
226
+ if _should_use_rich():
227
+ text = Text()
228
+ text.append(f"{prefix} ", style="success")
229
+ text.append(msg)
230
+ _get_console().print(text)
231
+ else:
232
+ print(f"{prefix} {msg}")
233
+
234
+
235
+ def debug(msg: str) -> None:
236
+ if not get_runtime_config().debug:
237
+ return
238
+ if _should_use_rich():
239
+ text = Text()
240
+ text.append("[gitwise debug] ", style="dim")
241
+ text.append(msg)
242
+ _get_stderr_console().print(text)
243
+ else:
244
+ print(f"[gitwise debug] {msg}", file=sys.stderr)
245
+
246
+
247
+ def print_json(data: Any) -> None:
248
+ if _JSON_PRETTY:
249
+ print(json.dumps(data, ensure_ascii=False, indent=2))
250
+ return
251
+ print(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
252
+
253
+
254
+ def print_blank() -> None:
255
+ if _should_use_rich():
256
+ _get_console().print()
257
+ else:
258
+ print()
259
+
260
+
261
+ def confirm(prompt: str) -> bool:
262
+ if not sys.stdin.isatty():
263
+ return False
264
+
265
+ if _should_use_rich():
266
+ try:
267
+ import importlib
268
+
269
+ prompt_mod = importlib.import_module("rich.prompt")
270
+ resp = prompt_mod.Prompt.ask(
271
+ prompt,
272
+ default="",
273
+ show_default=False,
274
+ show_choices=False,
275
+ console=_get_console(),
276
+ )
277
+ return resp.strip().lower() in confirm_responses()
278
+ except (EOFError, KeyboardInterrupt):
279
+ return False
280
+
281
+ try:
282
+ resp = input(prompt).strip().lower()
283
+ except (EOFError, KeyboardInterrupt):
284
+ return False
285
+ return resp in confirm_responses()
286
+
287
+
288
+ @contextmanager
289
+ def status(message: str) -> Iterator[None]:
290
+ if _should_use_rich():
291
+ with _get_console().status(message):
292
+ yield
293
+ return
294
+
295
+ if get_runtime_config().debug:
296
+ print_dim(message)
297
+ yield
298
+
299
+
300
+ def bat_pipe(text: str, language: str = "plain") -> None:
301
+ if not text:
302
+ return
303
+ if not text.endswith("\n"):
304
+ text += "\n"
305
+ if language is None or language == "plain":
306
+ for line in text.splitlines():
307
+ print(line)
308
+ return
309
+ cfg = get_runtime_config()
310
+ if cfg.has_bat and cfg.is_tty:
311
+ color_flag = "always" if not _color_disabled() else "never"
312
+ cmd = [
313
+ "bat",
314
+ "--style=plain",
315
+ "--pager=never",
316
+ f"--color={color_flag}",
317
+ "--language",
318
+ language,
319
+ ]
320
+ try:
321
+ r = subprocess.run(
322
+ cmd, input=text, text=True, check=False, stderr=subprocess.DEVNULL, timeout=120
323
+ )
324
+ if r.returncode == 0:
325
+ return
326
+ except OSError as e:
327
+ if get_runtime_config().debug:
328
+ sys.stderr.write(f"[gitwise debug] bat execution failed: {e}\n")
329
+ print(text, end="")
330
+
331
+
332
+ def print_header(text: str) -> None:
333
+ if _should_use_rich():
334
+ _get_console().print(text, style="bold")
335
+ else:
336
+ print(text)
337
+
338
+
339
+ def print_section(title: str) -> None:
340
+ if _should_use_rich():
341
+ print_blank()
342
+ _get_console().rule(f" {title} ", style="accent")
343
+ else:
344
+ print_blank()
345
+ print(f" {title}")
346
+
347
+
348
+ def print_bullet(text: str, *, icon: str = "•", accent: bool = False, indent: int = 2) -> None:
349
+ prefix = " " * max(0, indent)
350
+ if _should_use_rich():
351
+ row = Text()
352
+ row.append(prefix)
353
+ row.append(icon, style="accent" if accent else "secondary")
354
+ row.append(" ")
355
+ row.append(text)
356
+ _get_console().print(row)
357
+ else:
358
+ print(f"{prefix}{icon} {text}")
359
+
360
+
361
+ def _commit_type_style(message: str) -> str:
362
+ msg = message.strip().lower()
363
+ match = re.match(r"^([a-z]+)(\([^)]*\))?(!)?:", msg)
364
+ if not match:
365
+ if msg.startswith("merge "):
366
+ return "dim"
367
+ return "fg"
368
+ commit_type = match.group(1)
369
+ if commit_type == "feat":
370
+ return "success"
371
+ if commit_type == "fix":
372
+ return "warning"
373
+ if commit_type in {"docs", "style", "test", "ci", "build"}:
374
+ return "accent"
375
+ if commit_type in {"refactor", "perf"}:
376
+ return "brand"
377
+ if commit_type in {"revert"}:
378
+ return "error"
379
+ return "fg"
380
+
381
+
382
+ def print_commit_line(line: str, *, indent: int = 2) -> None:
383
+ prefix = " " * max(0, indent)
384
+ parts = line.strip().split(" ", 1)
385
+ if len(parts) != 2:
386
+ print_bullet(line.strip(), icon="-", accent=False, indent=indent)
387
+ return
388
+ short_hash, subject = parts[0], parts[1]
389
+ if not re.fullmatch(r"[0-9a-f]{7,40}", short_hash):
390
+ print_bullet(line.strip(), icon="-", accent=False, indent=indent)
391
+ return
392
+
393
+ if _should_use_rich():
394
+ text = Text()
395
+ text.append(prefix)
396
+ text.append(short_hash, style="secondary")
397
+ text.append(" ", style="dim")
398
+ text.append(subject, style=_commit_type_style(subject))
399
+ _get_console().print(text)
400
+ return
401
+
402
+ print(f"{prefix}- {short_hash} {subject}")
403
+
404
+
405
+ def _status_style(code: str) -> str:
406
+ normalized = code.strip().upper()
407
+ if normalized in {"??", "A", "M", "R", "C", "T", "U", "D"}:
408
+ if normalized in {"D", "U"}:
409
+ return "error"
410
+ if normalized in {"M", "R", "C", "T"}:
411
+ return "warning"
412
+ return "success"
413
+ return "secondary"
414
+
415
+
416
+ def _path_style_for_status(code: str) -> str:
417
+ normalized = code.strip().upper()
418
+ if normalized in {"??", "A"}:
419
+ return "success"
420
+ if normalized in {"D", "U"}:
421
+ return "error"
422
+ if normalized in {"M", "R", "C", "T"}:
423
+ return "warning"
424
+ return "fg"
425
+
426
+
427
+ def print_file_status(code: str, path: str, *, indent: int = 2) -> None:
428
+ status = code.strip() or "--"
429
+ prefix = " " * max(0, indent)
430
+ if _should_use_rich():
431
+ text = Text()
432
+ text.append(prefix)
433
+ text.append(status.rjust(2), style=_status_style(status))
434
+ text.append(" ", style="dim")
435
+ text.append(path, style=_path_style_for_status(status))
436
+ _get_console().print(text)
437
+ else:
438
+ print(f"{prefix}{status.rjust(2)} {path}")
439
+
440
+
441
+ def _append_diffstat_changes(text: Text, changes: str) -> None:
442
+ for char in changes:
443
+ if char == "+":
444
+ text.append(char, style="success")
445
+ elif char == "-":
446
+ text.append(char, style="error")
447
+ else:
448
+ text.append(char, style="secondary")
449
+
450
+
451
+ def print_diffstat(title: str, entries: list[dict[str, str]]) -> None:
452
+ if not entries:
453
+ return
454
+ if _should_use_rich():
455
+ print_header(title)
456
+ width = get_runtime_config().terminal_width
457
+ path_col = max(24, min(width // 2, max(visible_length(e["path"]) for e in entries)))
458
+ for entry in entries:
459
+ path = truncate(entry["path"], path_col)
460
+ padded_path = pad_right(path, path_col)
461
+ status = entry.get("status", "M")
462
+ changes = entry.get("changes", "")
463
+ row = Text()
464
+ row.append(" ")
465
+ row.append(padded_path, style=_path_style_for_status(status))
466
+ row.append(" ", style="dim")
467
+ _append_diffstat_changes(row, changes)
468
+ _get_console().print(row)
469
+ return
470
+
471
+ print(title)
472
+ for entry in entries:
473
+ print(f" {entry['path']} {entry.get('changes', '')}")
474
+
475
+
476
+ def print_summary_box(title: str, lines: list[str]) -> None:
477
+ if not lines:
478
+ return
479
+ if _should_use_rich():
480
+ _get_console().rule(f" {title} ", style="dim")
481
+ for line in lines:
482
+ row = Text()
483
+ row.append(" ", style="dim")
484
+ row.append(line)
485
+ _get_console().print(row)
486
+ return
487
+ print(title)
488
+ for line in lines:
489
+ print(f" {line}")
490
+
491
+
492
+ def print_bracket(label: str, value: str = "") -> None:
493
+ if _should_use_rich():
494
+ text = Text()
495
+ text.append(" [", style="secondary")
496
+ text.append(label, style="accent")
497
+ text.append("]", style="secondary")
498
+ if value:
499
+ text.append(f" {value}")
500
+ _get_console().print(text)
501
+ else:
502
+ if value:
503
+ print(f" [{label}] {value}")
504
+ else:
505
+ print(f" [{label}]")
506
+
507
+
508
+ def print_accent(text: str) -> None:
509
+ if _should_use_rich():
510
+ _get_console().print(text, style="accent")
511
+ else:
512
+ print(text)
513
+
514
+
515
+ def print_dim(text: str) -> None:
516
+ if _should_use_rich():
517
+ _get_console().print(text, style="dim")
518
+ else:
519
+ print(text)
520
+
521
+
522
+ def print_success(text: str) -> None:
523
+ if _should_use_rich():
524
+ _get_console().print(text, style="success")
525
+ else:
526
+ print(text)
527
+
528
+
529
+ def print_error_styled(text: str) -> None:
530
+ if _should_use_rich():
531
+ _get_console().print(text, style="error")
532
+ else:
533
+ print(text)
534
+
535
+
536
+ def print_kv(key: str, value: str) -> None:
537
+ width = get_runtime_config().terminal_width
538
+ key_col = min(24, width // 3)
539
+ padded_key = pad_right(f" {key}", key_col)
540
+ if _should_use_rich():
541
+ text = Text()
542
+ text.append(padded_key, style="secondary")
543
+ text.append(f" {value}")
544
+ _get_console().print(text)
545
+ else:
546
+ print(f"{padded_key} {value}")
547
+
548
+
549
+ def print_status_line(icon: str, label: str, status: str, ok_flag: bool = True) -> None:
550
+ if _should_use_rich():
551
+ console = _get_console()
552
+ width = get_runtime_config().terminal_width
553
+ icon_style = "success" if ok_flag else "error"
554
+ text = Text()
555
+ text.append(f" {icon} ", style=icon_style)
556
+ text.append(label, style="secondary")
557
+ used = visible_length(f" {icon} {label}") + visible_length(status) + 3
558
+ dots = max(1, width - used)
559
+ text.append(" " + "." * dots + " ", style="dim")
560
+ text.append(status, style=icon_style)
561
+ console.print(text)
562
+ else:
563
+ width = get_runtime_config().terminal_width
564
+ base = f" {icon} {label}"
565
+ if not status:
566
+ print(base)
567
+ return
568
+ used = visible_length(base) + visible_length(status) + 2
569
+ dots = max(1, width - used)
570
+ print(f"{base} {'·' * dots} {status}")
571
+
572
+
573
+ def print_table(
574
+ title: str,
575
+ columns: list[tuple[str, str]],
576
+ rows: list[list[str]],
577
+ *,
578
+ column_styles: list[str] | None = None,
579
+ highlight_rows: set[int] | None = None,
580
+ no_wrap_columns: set[int] | None = None,
581
+ min_widths: dict[int, int] | None = None,
582
+ max_widths: dict[int, int] | None = None,
583
+ overflow_columns: dict[int, Literal["fold", "crop", "ellipsis"]] | None = None,
584
+ column_ratios: dict[int, int] | None = None,
585
+ ) -> None:
586
+ if not _should_use_rich() or not rows:
587
+ if title:
588
+ print(title)
589
+ if not rows:
590
+ return
591
+ num_cols = len(columns)
592
+ col_widths = [len(c[0]) for c in columns]
593
+ for row in rows:
594
+ for i, cell in enumerate(row):
595
+ if i < num_cols:
596
+ col_widths[i] = max(col_widths[i], visible_length(cell))
597
+ headers = [columns[i][0] for i in range(num_cols)]
598
+ header_row = " ".join(pad_right(headers[i], col_widths[i]) for i in range(num_cols))
599
+ sep_row = " ".join("-" * col_widths[i] for i in range(num_cols))
600
+ print(f" {header_row}")
601
+ print(f" {sep_row}")
602
+ highlights = highlight_rows or set()
603
+ for idx, row in enumerate(rows):
604
+ prefix = " * " if idx in highlights else " "
605
+ padded = " ".join(
606
+ pad_right(row[i], col_widths[i]) if i < num_cols else row[i]
607
+ for i in range(len(row))
608
+ )
609
+ print(f"{prefix}{padded}")
610
+ return
611
+
612
+ console = _get_console()
613
+ table = Table(
614
+ title=title,
615
+ title_style="bold",
616
+ border_style="dim",
617
+ show_header=True,
618
+ header_style="bold.accent",
619
+ pad_edge=False,
620
+ padding=(0, 1),
621
+ expand=True,
622
+ )
623
+
624
+ no_wrap = no_wrap_columns or set()
625
+ mins = min_widths or {}
626
+ maxs = max_widths or {}
627
+ overflows = overflow_columns or {}
628
+ ratios = column_ratios or {}
629
+
630
+ for idx, (col_name, _col_key) in enumerate(columns):
631
+ no_wrap_value = (idx in no_wrap) if no_wrap_columns is not None else True
632
+ overflow_value: Literal["fold", "crop", "ellipsis"] = overflows.get(idx, "ellipsis")
633
+ table.add_column(
634
+ col_name,
635
+ no_wrap=no_wrap_value,
636
+ min_width=mins.get(idx),
637
+ max_width=maxs.get(idx),
638
+ overflow=overflow_value,
639
+ ratio=ratios.get(idx),
640
+ )
641
+
642
+ styles = column_styles or []
643
+ highlights = highlight_rows or set()
644
+
645
+ for idx, row_data in enumerate(rows):
646
+ if idx in highlights:
647
+ table.add_row(*row_data, style="accent")
648
+ else:
649
+ row_style = styles[idx % len(styles)] if styles else ""
650
+ table.add_row(*row_data, style=row_style)
651
+
652
+ console.print(table)