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