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.

Files changed (113) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +706 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1454 -0
  16. scc_cli/cli_org.py +1428 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +892 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +604 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1074 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1521 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/adapter.py +74 -0
  46. scc_cli/marketplace/compute.py +377 -0
  47. scc_cli/marketplace/constants.py +87 -0
  48. scc_cli/marketplace/managed.py +135 -0
  49. scc_cli/marketplace/materialize.py +723 -0
  50. scc_cli/marketplace/normalize.py +548 -0
  51. scc_cli/marketplace/render.py +257 -0
  52. scc_cli/marketplace/resolve.py +459 -0
  53. scc_cli/marketplace/schema.py +506 -0
  54. scc_cli/marketplace/sync.py +260 -0
  55. scc_cli/marketplace/team_cache.py +195 -0
  56. scc_cli/marketplace/team_fetch.py +688 -0
  57. scc_cli/marketplace/trust.py +244 -0
  58. scc_cli/models/__init__.py +41 -0
  59. scc_cli/models/exceptions.py +273 -0
  60. scc_cli/models/plugin_audit.py +434 -0
  61. scc_cli/org_templates.py +269 -0
  62. scc_cli/output_mode.py +167 -0
  63. scc_cli/panels.py +113 -0
  64. scc_cli/platform.py +350 -0
  65. scc_cli/profiles.py +960 -0
  66. scc_cli/remote.py +443 -0
  67. scc_cli/schemas/__init__.py +1 -0
  68. scc_cli/schemas/org-v1.schema.json +456 -0
  69. scc_cli/schemas/team-config.v1.schema.json +163 -0
  70. scc_cli/sessions.py +425 -0
  71. scc_cli/setup.py +588 -0
  72. scc_cli/source_resolver.py +470 -0
  73. scc_cli/stats.py +378 -0
  74. scc_cli/stores/__init__.py +13 -0
  75. scc_cli/stores/exception_store.py +251 -0
  76. scc_cli/subprocess_utils.py +88 -0
  77. scc_cli/teams.py +382 -0
  78. scc_cli/templates/__init__.py +2 -0
  79. scc_cli/templates/org/__init__.py +0 -0
  80. scc_cli/templates/org/minimal.json +19 -0
  81. scc_cli/templates/org/reference.json +74 -0
  82. scc_cli/templates/org/strict.json +38 -0
  83. scc_cli/templates/org/teams.json +42 -0
  84. scc_cli/templates/statusline.sh +75 -0
  85. scc_cli/theme.py +348 -0
  86. scc_cli/ui/__init__.py +124 -0
  87. scc_cli/ui/branding.py +68 -0
  88. scc_cli/ui/chrome.py +395 -0
  89. scc_cli/ui/dashboard/__init__.py +62 -0
  90. scc_cli/ui/dashboard/_dashboard.py +677 -0
  91. scc_cli/ui/dashboard/loaders.py +395 -0
  92. scc_cli/ui/dashboard/models.py +184 -0
  93. scc_cli/ui/dashboard/orchestrator.py +390 -0
  94. scc_cli/ui/formatters.py +443 -0
  95. scc_cli/ui/gate.py +350 -0
  96. scc_cli/ui/help.py +157 -0
  97. scc_cli/ui/keys.py +538 -0
  98. scc_cli/ui/list_screen.py +431 -0
  99. scc_cli/ui/picker.py +700 -0
  100. scc_cli/ui/prompts.py +200 -0
  101. scc_cli/ui/wizard.py +675 -0
  102. scc_cli/update.py +680 -0
  103. scc_cli/utils/__init__.py +39 -0
  104. scc_cli/utils/fixit.py +264 -0
  105. scc_cli/utils/fuzzy.py +124 -0
  106. scc_cli/utils/locks.py +101 -0
  107. scc_cli/utils/ttl.py +376 -0
  108. scc_cli/validate.py +455 -0
  109. scc_cli-1.4.1.dist-info/METADATA +369 -0
  110. scc_cli-1.4.1.dist-info/RECORD +113 -0
  111. scc_cli-1.4.1.dist-info/WHEEL +4 -0
  112. scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
  113. 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()