scc-cli 1.4.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.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

Files changed (112) 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 +683 -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 +1400 -0
  16. scc_cli/cli_org.py +1433 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +858 -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 +603 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1082 -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 +1405 -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/compute.py +377 -0
  46. scc_cli/marketplace/constants.py +87 -0
  47. scc_cli/marketplace/managed.py +135 -0
  48. scc_cli/marketplace/materialize.py +723 -0
  49. scc_cli/marketplace/normalize.py +548 -0
  50. scc_cli/marketplace/render.py +238 -0
  51. scc_cli/marketplace/resolve.py +459 -0
  52. scc_cli/marketplace/schema.py +502 -0
  53. scc_cli/marketplace/sync.py +257 -0
  54. scc_cli/marketplace/team_cache.py +195 -0
  55. scc_cli/marketplace/team_fetch.py +688 -0
  56. scc_cli/marketplace/trust.py +244 -0
  57. scc_cli/models/__init__.py +41 -0
  58. scc_cli/models/exceptions.py +273 -0
  59. scc_cli/models/plugin_audit.py +434 -0
  60. scc_cli/org_templates.py +269 -0
  61. scc_cli/output_mode.py +167 -0
  62. scc_cli/panels.py +113 -0
  63. scc_cli/platform.py +350 -0
  64. scc_cli/profiles.py +1034 -0
  65. scc_cli/remote.py +443 -0
  66. scc_cli/schemas/__init__.py +1 -0
  67. scc_cli/schemas/org-v1.schema.json +456 -0
  68. scc_cli/schemas/team-config.v1.schema.json +163 -0
  69. scc_cli/sessions.py +425 -0
  70. scc_cli/setup.py +582 -0
  71. scc_cli/source_resolver.py +470 -0
  72. scc_cli/stats.py +378 -0
  73. scc_cli/stores/__init__.py +13 -0
  74. scc_cli/stores/exception_store.py +251 -0
  75. scc_cli/subprocess_utils.py +88 -0
  76. scc_cli/teams.py +339 -0
  77. scc_cli/templates/__init__.py +2 -0
  78. scc_cli/templates/org/__init__.py +0 -0
  79. scc_cli/templates/org/minimal.json +19 -0
  80. scc_cli/templates/org/reference.json +74 -0
  81. scc_cli/templates/org/strict.json +38 -0
  82. scc_cli/templates/org/teams.json +42 -0
  83. scc_cli/templates/statusline.sh +75 -0
  84. scc_cli/theme.py +348 -0
  85. scc_cli/ui/__init__.py +124 -0
  86. scc_cli/ui/branding.py +68 -0
  87. scc_cli/ui/chrome.py +395 -0
  88. scc_cli/ui/dashboard/__init__.py +62 -0
  89. scc_cli/ui/dashboard/_dashboard.py +669 -0
  90. scc_cli/ui/dashboard/loaders.py +369 -0
  91. scc_cli/ui/dashboard/models.py +184 -0
  92. scc_cli/ui/dashboard/orchestrator.py +337 -0
  93. scc_cli/ui/formatters.py +443 -0
  94. scc_cli/ui/gate.py +350 -0
  95. scc_cli/ui/help.py +157 -0
  96. scc_cli/ui/keys.py +521 -0
  97. scc_cli/ui/list_screen.py +431 -0
  98. scc_cli/ui/picker.py +700 -0
  99. scc_cli/ui/prompts.py +200 -0
  100. scc_cli/ui/wizard.py +490 -0
  101. scc_cli/update.py +680 -0
  102. scc_cli/utils/__init__.py +39 -0
  103. scc_cli/utils/fixit.py +264 -0
  104. scc_cli/utils/fuzzy.py +124 -0
  105. scc_cli/utils/locks.py +101 -0
  106. scc_cli/utils/ttl.py +376 -0
  107. scc_cli/validate.py +455 -0
  108. scc_cli-1.4.0.dist-info/METADATA +369 -0
  109. scc_cli-1.4.0.dist-info/RECORD +112 -0
  110. scc_cli-1.4.0.dist-info/WHEEL +4 -0
  111. scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
  112. scc_cli-1.4.0.dist-info/licenses/LICENSE +21 -0
scc_cli/cli_support.py ADDED
@@ -0,0 +1,322 @@
1
+ """
2
+ Provide CLI commands for support and diagnostics.
3
+
4
+ Generate support bundles with diagnostic information. Include secret
5
+ and path redaction for safe sharing.
6
+ """
7
+
8
+ import json
9
+ import platform
10
+ import re
11
+ import sys
12
+ import zipfile
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import typer
18
+
19
+ from . import __version__, config, doctor
20
+ from .cli_common import console, handle_errors
21
+ from .json_output import build_envelope
22
+ from .kinds import Kind
23
+ from .output_mode import json_output_mode, print_json, set_pretty_mode
24
+
25
+ # ─────────────────────────────────────────────────────────────────────────────
26
+ # Support App
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ support_app = typer.Typer(
30
+ name="support",
31
+ help="Support and diagnostic commands.",
32
+ no_args_is_help=True,
33
+ )
34
+
35
+
36
+ # ─────────────────────────────────────────────────────────────────────────────
37
+ # Secret Redaction (Pure Function)
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ # Keys that should have their values redacted
41
+ SECRET_KEY_PATTERNS = [
42
+ r"^auth$",
43
+ r".*token.*",
44
+ r".*api[_-]?key.*",
45
+ r".*apikey.*",
46
+ r".*password.*",
47
+ r".*secret.*",
48
+ r"^authorization$",
49
+ r".*credential.*",
50
+ ]
51
+
52
+ # Compiled regex patterns (case-insensitive)
53
+ _SECRET_PATTERNS = [re.compile(p, re.IGNORECASE) for p in SECRET_KEY_PATTERNS]
54
+
55
+
56
+ def _is_secret_key(key: str) -> bool:
57
+ """Check if a key name indicates a secret value."""
58
+ return any(pattern.match(key) for pattern in _SECRET_PATTERNS)
59
+
60
+
61
+ def redact_secrets(data: dict[str, Any]) -> dict[str, Any]:
62
+ """Redact secret values from a dictionary.
63
+
64
+ Recursively processes nested dicts and lists.
65
+
66
+ Args:
67
+ data: Dictionary potentially containing secrets
68
+
69
+ Returns:
70
+ Copy of dict with secret values replaced by [REDACTED]
71
+ """
72
+ result: dict[str, Any] = {}
73
+
74
+ for key, value in data.items():
75
+ if _is_secret_key(key) and isinstance(value, str):
76
+ result[key] = "[REDACTED]"
77
+ elif isinstance(value, dict):
78
+ result[key] = redact_secrets(value)
79
+ elif isinstance(value, list):
80
+ result[key] = [
81
+ redact_secrets(item) if isinstance(item, dict) else item for item in value
82
+ ]
83
+ else:
84
+ result[key] = value
85
+
86
+ return result
87
+
88
+
89
+ # ─────────────────────────────────────────────────────────────────────────────
90
+ # Path Redaction (Pure Function)
91
+ # ─────────────────────────────────────────────────────────────────────────────
92
+
93
+
94
+ def redact_paths(data: dict[str, Any], redact: bool = True) -> dict[str, Any]:
95
+ """Redact home directory paths from a dictionary.
96
+
97
+ Replaces absolute paths containing the home directory with ~ prefix.
98
+
99
+ Args:
100
+ data: Dictionary potentially containing paths
101
+ redact: If False, return data unchanged
102
+
103
+ Returns:
104
+ Copy of dict with home paths redacted
105
+ """
106
+ if not redact:
107
+ return data
108
+
109
+ home = str(Path.home())
110
+ result: dict[str, Any] = {}
111
+
112
+ for key, value in data.items():
113
+ if isinstance(value, str) and home in value:
114
+ result[key] = value.replace(home, "~")
115
+ elif isinstance(value, dict):
116
+ result[key] = redact_paths(value, redact=redact)
117
+ elif isinstance(value, list):
118
+ result[key] = [
119
+ redact_paths(item, redact=redact)
120
+ if isinstance(item, dict)
121
+ else (item.replace(home, "~") if isinstance(item, str) and home in item else item)
122
+ for item in value
123
+ ]
124
+ else:
125
+ result[key] = value
126
+
127
+ return result
128
+
129
+
130
+ # ─────────────────────────────────────────────────────────────────────────────
131
+ # Bundle Data Collection (Pure Function)
132
+ # ─────────────────────────────────────────────────────────────────────────────
133
+
134
+
135
+ def build_bundle_data(
136
+ redact_paths_flag: bool = True,
137
+ workspace_path: Path | None = None,
138
+ ) -> dict[str, Any]:
139
+ """Build support bundle data.
140
+
141
+ Collects system info, config, doctor output, and other diagnostics.
142
+ All secrets are automatically redacted.
143
+
144
+ Args:
145
+ redact_paths_flag: Whether to redact home directory paths
146
+ workspace_path: Optional workspace to include in diagnostics
147
+
148
+ Returns:
149
+ Dictionary with all bundle data
150
+ """
151
+ # System information
152
+ system_info = {
153
+ "platform": platform.system(),
154
+ "platform_version": platform.version(),
155
+ "platform_release": platform.release(),
156
+ "machine": platform.machine(),
157
+ "python_version": sys.version,
158
+ "python_implementation": platform.python_implementation(),
159
+ }
160
+
161
+ # CLI version
162
+ cli_version = __version__
163
+
164
+ # Timestamp
165
+ generated_at = datetime.now(timezone.utc).isoformat()
166
+
167
+ # Load and redact config
168
+ try:
169
+ user_config = config.load_config()
170
+ user_config = redact_secrets(user_config)
171
+ except Exception:
172
+ user_config = {"error": "Failed to load config"}
173
+
174
+ # Load and redact org config
175
+ try:
176
+ org_config = config.load_cached_org_config()
177
+ if org_config:
178
+ org_config = redact_secrets(org_config)
179
+ except Exception:
180
+ org_config = {"error": "Failed to load org config"}
181
+
182
+ # Run doctor checks
183
+ try:
184
+ doctor_result = doctor.run_doctor(workspace_path)
185
+ doctor_data = doctor.build_doctor_json_data(doctor_result)
186
+ except Exception as e:
187
+ doctor_data = {"error": f"Failed to run doctor: {e}"}
188
+
189
+ # Build bundle data
190
+ bundle_data: dict[str, Any] = {
191
+ "generated_at": generated_at,
192
+ "cli_version": cli_version,
193
+ "system": system_info,
194
+ "config": user_config,
195
+ "org_config": org_config,
196
+ "doctor": doctor_data,
197
+ }
198
+
199
+ # Include workspace info if provided
200
+ if workspace_path:
201
+ bundle_data["workspace"] = str(workspace_path)
202
+
203
+ # Apply path redaction if enabled
204
+ if redact_paths_flag:
205
+ bundle_data = redact_paths(bundle_data)
206
+
207
+ return bundle_data
208
+
209
+
210
+ # ─────────────────────────────────────────────────────────────────────────────
211
+ # Bundle File Creation
212
+ # ─────────────────────────────────────────────────────────────────────────────
213
+
214
+
215
+ def get_default_bundle_path() -> Path:
216
+ """Get default path for support bundle.
217
+
218
+ Returns:
219
+ Path with timestamp-based filename
220
+ """
221
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
222
+ return Path.cwd() / f"scc-support-bundle-{timestamp}.zip"
223
+
224
+
225
+ def create_bundle(
226
+ output_path: Path,
227
+ redact_paths_flag: bool = True,
228
+ workspace_path: Path | None = None,
229
+ ) -> dict[str, Any]:
230
+ """Create a support bundle zip file.
231
+
232
+ Args:
233
+ output_path: Path for the output zip file
234
+ redact_paths_flag: Whether to redact home directory paths
235
+ workspace_path: Optional workspace to include in diagnostics
236
+
237
+ Returns:
238
+ The bundle data that was written to the manifest
239
+ """
240
+ bundle_data = build_bundle_data(
241
+ redact_paths_flag=redact_paths_flag,
242
+ workspace_path=workspace_path,
243
+ )
244
+
245
+ # Create zip file with manifest
246
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
247
+ manifest_json = json.dumps(bundle_data, indent=2)
248
+ zf.writestr("manifest.json", manifest_json)
249
+
250
+ return bundle_data
251
+
252
+
253
+ # ─────────────────────────────────────────────────────────────────────────────
254
+ # Support Bundle Command
255
+ # ─────────────────────────────────────────────────────────────────────────────
256
+
257
+
258
+ @support_app.command("bundle")
259
+ @handle_errors
260
+ def support_bundle_cmd(
261
+ output: str | None = typer.Option(
262
+ None,
263
+ "--output",
264
+ "-o",
265
+ help="Output path for the bundle zip file",
266
+ ),
267
+ json_output: bool = typer.Option(
268
+ False,
269
+ "--json",
270
+ help="Output manifest as JSON instead of creating zip",
271
+ ),
272
+ pretty: bool = typer.Option(
273
+ False,
274
+ "--pretty",
275
+ help="Pretty-print JSON output (implies --json)",
276
+ ),
277
+ no_redact_paths: bool = typer.Option(
278
+ False,
279
+ "--no-redact-paths",
280
+ help="Don't redact home directory paths",
281
+ ),
282
+ ) -> None:
283
+ """Generate a support bundle for troubleshooting.
284
+
285
+ Creates a zip file containing:
286
+ - System information (platform, Python version)
287
+ - CLI configuration (secrets redacted)
288
+ - Doctor output (health check results)
289
+ - Diagnostic information
290
+
291
+ The bundle is safe to share - all sensitive data is redacted.
292
+ """
293
+ # --pretty implies --json
294
+ if pretty:
295
+ json_output = True
296
+ set_pretty_mode(True)
297
+
298
+ redact_paths_flag = not no_redact_paths
299
+
300
+ if json_output:
301
+ with json_output_mode():
302
+ bundle_data = build_bundle_data(redact_paths_flag=redact_paths_flag)
303
+ envelope = build_envelope(Kind.SUPPORT_BUNDLE, data=bundle_data)
304
+ print_json(envelope)
305
+ raise typer.Exit(0)
306
+
307
+ # Create the bundle zip file
308
+ output_path = Path(output) if output else get_default_bundle_path()
309
+
310
+ console.print("[cyan]Generating support bundle...[/cyan]")
311
+ create_bundle(
312
+ output_path=output_path,
313
+ redact_paths_flag=redact_paths_flag,
314
+ )
315
+
316
+ console.print()
317
+ console.print(f"[green]Support bundle created:[/green] {output_path}")
318
+ console.print()
319
+ console.print("[dim]The bundle contains diagnostic information with secrets redacted.[/dim]")
320
+ console.print("[dim]You can share this file safely with support.[/dim]")
321
+
322
+ raise typer.Exit(0)