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.
- 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 +683 -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 +1400 -0
- scc_cli/cli_org.py +1433 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +858 -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 +603 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1082 -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 +1405 -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/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 +238 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +502 -0
- scc_cli/marketplace/sync.py +257 -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 +1034 -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 +582 -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 +339 -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 +669 -0
- scc_cli/ui/dashboard/loaders.py +369 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +337 -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 +521 -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 +490 -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.0.dist-info/METADATA +369 -0
- scc_cli-1.4.0.dist-info/RECORD +112 -0
- scc_cli-1.4.0.dist-info/WHEEL +4 -0
- scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
- 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)
|