scc-cli 1.4.1__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.
Potentially problematic release.
This version of scc-cli might be problematic. Click here for more details.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +259 -0
- scc_cli/cli_admin.py +706 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1454 -0
- scc_cli/cli_org.py +1428 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +892 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +604 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1074 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/exit_codes.py +55 -0
- scc_cli/git.py +1521 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +257 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +260 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +588 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +382 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +677 -0
- scc_cli/ui/dashboard/loaders.py +395 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +390 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +538 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +675 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.1.dist-info/METADATA +369 -0
- scc_cli-1.4.1.dist-info/RECORD +113 -0
- scc_cli-1.4.1.dist-info/WHEEL +4 -0
- scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
scc_cli/cli_audit.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Provide CLI commands for plugin audit functionality.
|
|
2
|
+
|
|
3
|
+
Audit installed Claude Code plugins via the `scc audit plugins` command,
|
|
4
|
+
including manifest validation and MCP server/hooks discovery.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
from scc_cli.audit.reader import audit_all_plugins
|
|
20
|
+
from scc_cli.constants import AGENT_CONFIG_DIR
|
|
21
|
+
from scc_cli.models.plugin_audit import (
|
|
22
|
+
AuditOutput,
|
|
23
|
+
ManifestStatus,
|
|
24
|
+
PluginAuditResult,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# Create the audit sub-app
|
|
30
|
+
audit_app = typer.Typer(
|
|
31
|
+
name="audit",
|
|
32
|
+
help="Audit installed plugins and configurations.",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_claude_dir() -> Path:
|
|
38
|
+
"""Get the Claude Code directory path."""
|
|
39
|
+
return Path.home() / AGENT_CONFIG_DIR
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def format_status(status: str) -> str:
|
|
43
|
+
"""Format status with color for Rich output."""
|
|
44
|
+
if status == "clean":
|
|
45
|
+
return "[dim]clean[/dim]"
|
|
46
|
+
elif status == "parsed":
|
|
47
|
+
return "[green]parsed[/green]"
|
|
48
|
+
elif status == "malformed":
|
|
49
|
+
return "[red]malformed[/red]"
|
|
50
|
+
elif status == "unreadable":
|
|
51
|
+
return "[yellow]unreadable[/yellow]"
|
|
52
|
+
elif status == "not installed":
|
|
53
|
+
return "[dim]not installed[/dim]"
|
|
54
|
+
return status
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_human_output(output: AuditOutput) -> None:
|
|
58
|
+
"""Render audit output in human-readable format."""
|
|
59
|
+
# Header
|
|
60
|
+
console.print()
|
|
61
|
+
header_body = Text()
|
|
62
|
+
header_body.append("Plugin Audit Report", style="bold")
|
|
63
|
+
header_body.append("\n")
|
|
64
|
+
header_body.append(f"Discovered {output.total_plugins} plugin(s)", style="dim")
|
|
65
|
+
console.print(
|
|
66
|
+
Panel(
|
|
67
|
+
header_body,
|
|
68
|
+
border_style="cyan",
|
|
69
|
+
padding=(0, 1),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
console.print()
|
|
73
|
+
|
|
74
|
+
if output.total_plugins == 0:
|
|
75
|
+
console.print("[dim]No plugins installed.[/dim]")
|
|
76
|
+
console.print()
|
|
77
|
+
_print_disclaimer()
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Create table for plugins with rounded borders
|
|
81
|
+
table = Table(
|
|
82
|
+
show_header=True,
|
|
83
|
+
header_style="bold",
|
|
84
|
+
box=box.ROUNDED,
|
|
85
|
+
border_style="dim",
|
|
86
|
+
padding=(0, 1),
|
|
87
|
+
)
|
|
88
|
+
table.add_column("Plugin", style="cyan", no_wrap=True)
|
|
89
|
+
table.add_column("Version", style="dim")
|
|
90
|
+
table.add_column("Status", justify="center")
|
|
91
|
+
table.add_column("MCP", justify="right", style="green")
|
|
92
|
+
table.add_column("Hooks", justify="right", style="yellow")
|
|
93
|
+
|
|
94
|
+
for plugin in output.plugins:
|
|
95
|
+
# Format MCP server count
|
|
96
|
+
mcp_count = 0
|
|
97
|
+
if plugin.manifests and plugin.manifests.mcp.status == ManifestStatus.PARSED:
|
|
98
|
+
mcp_count = len(plugin.manifests.mcp_servers)
|
|
99
|
+
mcp_display = str(mcp_count) if mcp_count > 0 else "[dim]-[/dim]"
|
|
100
|
+
|
|
101
|
+
# Format hooks count
|
|
102
|
+
hooks_count = 0
|
|
103
|
+
if plugin.manifests and plugin.manifests.hooks.status == ManifestStatus.PARSED:
|
|
104
|
+
hooks_count = len(plugin.manifests.hooks_info)
|
|
105
|
+
hooks_display = str(hooks_count) if hooks_count > 0 else "[dim]-[/dim]"
|
|
106
|
+
|
|
107
|
+
table.add_row(
|
|
108
|
+
plugin.plugin_name,
|
|
109
|
+
plugin.version,
|
|
110
|
+
format_status(plugin.status_summary),
|
|
111
|
+
mcp_display,
|
|
112
|
+
hooks_display,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
# Show details for plugins with declarations
|
|
119
|
+
for plugin in output.plugins:
|
|
120
|
+
if plugin.manifests and plugin.manifests.has_declarations:
|
|
121
|
+
_render_plugin_details(plugin)
|
|
122
|
+
|
|
123
|
+
# Show problems in a warning panel
|
|
124
|
+
problem_plugins = [p for p in output.plugins if p.has_ci_failures]
|
|
125
|
+
if problem_plugins:
|
|
126
|
+
_render_problems_panel(problem_plugins)
|
|
127
|
+
|
|
128
|
+
# Disclaimer
|
|
129
|
+
_print_disclaimer()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _render_plugin_details(plugin: PluginAuditResult) -> None:
|
|
133
|
+
"""Render details for a plugin with declarations."""
|
|
134
|
+
if not plugin.manifests:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Create a details grid
|
|
138
|
+
details = Text()
|
|
139
|
+
details.append(f" {plugin.plugin_name}", style="bold cyan")
|
|
140
|
+
details.append("\n")
|
|
141
|
+
|
|
142
|
+
# Show MCP servers
|
|
143
|
+
if plugin.manifests.mcp_servers:
|
|
144
|
+
details.append(" MCP Servers: ", style="dim")
|
|
145
|
+
server_names = [s.name for s in plugin.manifests.mcp_servers]
|
|
146
|
+
details.append(", ".join(server_names), style="green")
|
|
147
|
+
details.append("\n")
|
|
148
|
+
|
|
149
|
+
# Show hooks
|
|
150
|
+
if plugin.manifests.hooks_info:
|
|
151
|
+
details.append(" Hooks: ", style="dim")
|
|
152
|
+
hook_events = [h.event for h in plugin.manifests.hooks_info]
|
|
153
|
+
details.append(", ".join(hook_events), style="yellow")
|
|
154
|
+
details.append("\n")
|
|
155
|
+
|
|
156
|
+
console.print(details)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _render_problems_panel(plugins: list[PluginAuditResult]) -> None:
|
|
160
|
+
"""Render a warning panel for all plugins with CI failures."""
|
|
161
|
+
problems_text = Text()
|
|
162
|
+
|
|
163
|
+
for i, plugin in enumerate(plugins):
|
|
164
|
+
if not plugin.manifests:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if i > 0:
|
|
168
|
+
problems_text.append("\n")
|
|
169
|
+
|
|
170
|
+
problems_text.append(f"⚠ {plugin.plugin_name}", style="bold red")
|
|
171
|
+
problems_text.append("\n")
|
|
172
|
+
|
|
173
|
+
# Check MCP manifest
|
|
174
|
+
if plugin.manifests.mcp.has_problems:
|
|
175
|
+
mcp = plugin.manifests.mcp
|
|
176
|
+
if mcp.status == ManifestStatus.MALFORMED and mcp.error:
|
|
177
|
+
problems_text.append(" .mcp.json: ", style="dim")
|
|
178
|
+
problems_text.append(f"malformed ({mcp.error.format()})", style="red")
|
|
179
|
+
problems_text.append("\n")
|
|
180
|
+
elif mcp.status == ManifestStatus.UNREADABLE:
|
|
181
|
+
problems_text.append(" .mcp.json: ", style="dim")
|
|
182
|
+
problems_text.append(f"unreadable ({mcp.error_message})", style="yellow")
|
|
183
|
+
problems_text.append("\n")
|
|
184
|
+
|
|
185
|
+
# Check hooks manifest
|
|
186
|
+
if plugin.manifests.hooks.has_problems:
|
|
187
|
+
hooks = plugin.manifests.hooks
|
|
188
|
+
if hooks.status == ManifestStatus.MALFORMED and hooks.error:
|
|
189
|
+
problems_text.append(" hooks/hooks.json: ", style="dim")
|
|
190
|
+
problems_text.append(f"malformed ({hooks.error.format()})", style="red")
|
|
191
|
+
problems_text.append("\n")
|
|
192
|
+
elif hooks.status == ManifestStatus.UNREADABLE:
|
|
193
|
+
problems_text.append(" hooks/hooks.json: ", style="dim")
|
|
194
|
+
problems_text.append(f"unreadable ({hooks.error_message})", style="yellow")
|
|
195
|
+
problems_text.append("\n")
|
|
196
|
+
|
|
197
|
+
console.print(
|
|
198
|
+
Panel(
|
|
199
|
+
problems_text,
|
|
200
|
+
title="[bold yellow]Manifest Problems[/bold yellow]",
|
|
201
|
+
border_style="yellow",
|
|
202
|
+
padding=(0, 1),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _print_disclaimer() -> None:
|
|
209
|
+
"""Print the informational disclaimer."""
|
|
210
|
+
console.print("[dim]ℹ Informational only; SCC does not enforce plugin internals.[/dim]")
|
|
211
|
+
console.print()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def render_json_output(output: AuditOutput) -> None:
|
|
215
|
+
"""Render audit output as JSON."""
|
|
216
|
+
data = output.to_dict()
|
|
217
|
+
console.print(json.dumps(data, indent=2))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@audit_app.command(name="plugins")
|
|
221
|
+
def audit_plugins_cmd(
|
|
222
|
+
as_json: bool = typer.Option(
|
|
223
|
+
False,
|
|
224
|
+
"--json",
|
|
225
|
+
help="Output as JSON with schemaVersion for CI integration.",
|
|
226
|
+
),
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Audit installed Claude Code plugins.
|
|
229
|
+
|
|
230
|
+
Shows manifest status, MCP servers, and hooks for all installed plugins.
|
|
231
|
+
|
|
232
|
+
Exit codes:
|
|
233
|
+
- 0: All plugins parsed successfully (or no plugins installed)
|
|
234
|
+
- 1: One or more plugins have malformed or unreadable manifests
|
|
235
|
+
"""
|
|
236
|
+
claude_dir = get_claude_dir()
|
|
237
|
+
output = audit_all_plugins(claude_dir)
|
|
238
|
+
|
|
239
|
+
if as_json:
|
|
240
|
+
render_json_output(output)
|
|
241
|
+
else:
|
|
242
|
+
render_human_output(output)
|
|
243
|
+
|
|
244
|
+
# Exit with appropriate code for CI
|
|
245
|
+
raise typer.Exit(code=output.exit_code)
|
scc_cli/cli_common.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Common Utilities.
|
|
3
|
+
|
|
4
|
+
Shared utilities, constants, and decorators used across all CLI modules.
|
|
5
|
+
This module is extracted to prevent circular imports and enable clean composition.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Any, TypeVar, cast
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from .errors import SCCError
|
|
18
|
+
from .exit_codes import EXIT_CANCELLED
|
|
19
|
+
from .output_mode import is_json_command_mode, is_json_mode
|
|
20
|
+
from .panels import create_warning_panel
|
|
21
|
+
from .ui.prompts import render_error
|
|
22
|
+
|
|
23
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
24
|
+
|
|
25
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
# Display Constants
|
|
27
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
# Maximum length for displaying file paths before truncation
|
|
30
|
+
MAX_DISPLAY_PATH_LENGTH = 50
|
|
31
|
+
# Characters to keep when truncating (MAX - 3 for "...")
|
|
32
|
+
PATH_TRUNCATE_LENGTH = 47
|
|
33
|
+
# Terminal width threshold for wide mode tables
|
|
34
|
+
WIDE_MODE_THRESHOLD = 110
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
# Shared Console and State
|
|
39
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
console = Console()
|
|
42
|
+
err_console = Console(stderr=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AppState:
|
|
46
|
+
"""Global application state for CLI flags."""
|
|
47
|
+
|
|
48
|
+
debug: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
state = AppState()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
# Error Boundary Decorator
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def handle_errors(func: F) -> F:
|
|
60
|
+
"""Catch SCCError exceptions and render user-friendly error output.
|
|
61
|
+
|
|
62
|
+
Wrap CLI command functions to provide consistent error handling:
|
|
63
|
+
- SCCError: Render with render_error and exit with error's exit_code
|
|
64
|
+
- KeyboardInterrupt: Print cancellation message and exit 130
|
|
65
|
+
- Other exceptions: Show warning panel (or full traceback with --debug)
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
func: The CLI command function to wrap.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Wrapped function with error handling.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@wraps(func)
|
|
75
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
76
|
+
try:
|
|
77
|
+
return func(*args, **kwargs)
|
|
78
|
+
except SCCError as e:
|
|
79
|
+
if is_json_command_mode():
|
|
80
|
+
raise
|
|
81
|
+
target_console = err_console if is_json_mode() else console
|
|
82
|
+
render_error(target_console, e, debug=state.debug)
|
|
83
|
+
raise typer.Exit(e.exit_code)
|
|
84
|
+
except KeyboardInterrupt:
|
|
85
|
+
if is_json_command_mode():
|
|
86
|
+
raise
|
|
87
|
+
target_console = err_console if is_json_mode() else console
|
|
88
|
+
target_console.print("\n[dim]Operation cancelled.[/dim]")
|
|
89
|
+
raise typer.Exit(EXIT_CANCELLED)
|
|
90
|
+
except (typer.Exit, SystemExit):
|
|
91
|
+
# Let typer exits pass through
|
|
92
|
+
raise
|
|
93
|
+
except Exception as e:
|
|
94
|
+
if is_json_command_mode():
|
|
95
|
+
raise
|
|
96
|
+
# Unexpected errors
|
|
97
|
+
target_console = err_console if is_json_mode() else console
|
|
98
|
+
if state.debug:
|
|
99
|
+
target_console.print_exception()
|
|
100
|
+
else:
|
|
101
|
+
target_console.print(
|
|
102
|
+
create_warning_panel(
|
|
103
|
+
"Unexpected Error",
|
|
104
|
+
str(e),
|
|
105
|
+
"Run with --debug for full traceback",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
raise typer.Exit(5)
|
|
109
|
+
|
|
110
|
+
return cast(F, wrapper)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
# UI Helpers (Consistent Aesthetic)
|
|
115
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def render_responsive_table(
|
|
119
|
+
title: str,
|
|
120
|
+
columns: list[tuple[str, str]], # (header, style)
|
|
121
|
+
rows: list[list[str]],
|
|
122
|
+
wide_columns: list[tuple[str, str]] | None = None, # Extra columns for wide mode
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Render a table that adapts to terminal width.
|
|
125
|
+
|
|
126
|
+
Display base columns on narrow terminals, adding extra columns when
|
|
127
|
+
terminal width exceeds WIDE_MODE_THRESHOLD.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
title: Table title displayed above the table.
|
|
131
|
+
columns: Base columns as list of (header, style) tuples.
|
|
132
|
+
rows: Data rows where each row contains values for all columns
|
|
133
|
+
(base + wide). Extra values are ignored on narrow terminals.
|
|
134
|
+
wide_columns: Additional columns shown only on wide terminals.
|
|
135
|
+
"""
|
|
136
|
+
width = console.width
|
|
137
|
+
wide_mode = width >= WIDE_MODE_THRESHOLD
|
|
138
|
+
|
|
139
|
+
table = Table(
|
|
140
|
+
title=f"[bold cyan]{title}[/bold cyan]",
|
|
141
|
+
box=box.ROUNDED,
|
|
142
|
+
header_style="bold cyan",
|
|
143
|
+
expand=True,
|
|
144
|
+
show_lines=False,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Add base columns
|
|
148
|
+
for header, style in columns:
|
|
149
|
+
table.add_column(header, style=style)
|
|
150
|
+
|
|
151
|
+
# Add extra columns in wide mode
|
|
152
|
+
if wide_mode and wide_columns:
|
|
153
|
+
for header, style in wide_columns:
|
|
154
|
+
table.add_column(header, style=style)
|
|
155
|
+
|
|
156
|
+
# Add rows
|
|
157
|
+
for row in rows:
|
|
158
|
+
if wide_mode and wide_columns:
|
|
159
|
+
table.add_row(*row)
|
|
160
|
+
else:
|
|
161
|
+
# Truncate to base columns only
|
|
162
|
+
table.add_row(*row[: len(columns)])
|
|
163
|
+
|
|
164
|
+
console.print()
|
|
165
|
+
console.print(table)
|
|
166
|
+
console.print()
|