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,444 @@
1
+ """Display formatting helpers for domain types.
2
+
3
+ This module provides pure functions to convert domain objects into display
4
+ representations suitable for the interactive UI. Each formatter transforms
5
+ a domain type into a ListItem for use in pickers and lists.
6
+
7
+ Example:
8
+ >>> from scc_cli.docker.core import ContainerInfo
9
+ >>> from scc_cli.ui.formatters import format_container
10
+ >>>
11
+ >>> container = ContainerInfo(id="abc123", name="scc-main", status="Up 2 hours")
12
+ >>> item = format_container(container)
13
+ >>> print(item.label) # scc-main
14
+ >>> print(item.description) # Up 2 hours
15
+
16
+ The formatters follow a consistent pattern:
17
+ - Input: Domain type (dataclass or dict)
18
+ - Output: ListItem with label, description, metadata, and optional governance status
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from datetime import datetime, timezone
24
+ from typing import TYPE_CHECKING, Any, TypedDict
25
+
26
+ from ..docker.core import ContainerInfo
27
+ from ..git import WorktreeInfo, get_display_branch
28
+ from ..theme import Indicators
29
+ from .list_screen import ListItem
30
+
31
+ if TYPE_CHECKING:
32
+ from ..contexts import WorkContext
33
+
34
+
35
+ # ═══════════════════════════════════════════════════════════════════════════════
36
+ # TypedDict Metadata Definitions (enables mypy type checking and IDE autocomplete)
37
+ # ═══════════════════════════════════════════════════════════════════════════════
38
+
39
+
40
+ class ContainerMetadata(TypedDict):
41
+ """Metadata for container list items.
42
+
43
+ Keys:
44
+ running: "yes" or "no" indicating container state.
45
+ id: Short (12-char) container ID for display.
46
+ """
47
+
48
+ running: str
49
+ id: str
50
+
51
+
52
+ class WorktreeMetadata(TypedDict):
53
+ """Metadata for worktree list items.
54
+
55
+ Keys:
56
+ path: Full filesystem path to the worktree.
57
+ current: "yes" or "no" indicating if this is the current worktree.
58
+ """
59
+
60
+ path: str
61
+ current: str
62
+
63
+
64
+ class ContextMetadata(TypedDict):
65
+ """Metadata for work context list items.
66
+
67
+ Keys:
68
+ team: Team/profile name.
69
+ repo: Repository name.
70
+ worktree: Worktree directory name.
71
+ path: Full filesystem path.
72
+ pinned: "yes" or "no".
73
+ running: "yes", "no", or "" (unknown).
74
+ current_branch: "yes", "no", or "" (unknown).
75
+ """
76
+
77
+ team: str
78
+ repo: str
79
+ worktree: str
80
+ path: str
81
+ pinned: str
82
+ running: str
83
+ current_branch: str
84
+
85
+
86
+ def format_team(
87
+ team: dict[str, Any], *, current_team: str | None = None
88
+ ) -> ListItem[dict[str, Any]]:
89
+ """Format a team dict for display in a picker.
90
+
91
+ Args:
92
+ team: Team dictionary with name and optional metadata.
93
+ current_team: Currently selected team name (marked with indicator).
94
+
95
+ Returns:
96
+ ListItem suitable for ListScreen display.
97
+
98
+ Example:
99
+ >>> team = {"name": "platform", "description": "Platform team"}
100
+ >>> item = format_team(team, current_team="platform")
101
+ >>> item.label
102
+ '✓ platform'
103
+ """
104
+ name = team.get("name", "unknown")
105
+ description = team.get("description", "")
106
+ is_current = current_team is not None and name == current_team
107
+
108
+ # Build label with current indicator
109
+ label = f"{Indicators.get('PASS')} {name}" if is_current else name
110
+
111
+ # Check for credential/governance status
112
+ governance_status: str | None = None
113
+ credential_status = team.get("credential_status")
114
+ if credential_status == "expired":
115
+ governance_status = "blocked"
116
+ elif credential_status == "expiring":
117
+ governance_status = "warning"
118
+
119
+ # Build description parts
120
+ desc_parts: list[str] = []
121
+ if description:
122
+ desc_parts.append(description)
123
+ if credential_status == "expired":
124
+ desc_parts.append("(credentials expired)")
125
+ elif credential_status == "expiring":
126
+ desc_parts.append("(credentials expiring)")
127
+
128
+ return ListItem(
129
+ value=team,
130
+ label=label,
131
+ description=" ".join(desc_parts),
132
+ governance_status=governance_status,
133
+ )
134
+
135
+
136
+ def format_container(container: ContainerInfo) -> ListItem[ContainerInfo]:
137
+ """Format a container for display in a picker or list.
138
+
139
+ Args:
140
+ container: Container information from Docker.
141
+
142
+ Returns:
143
+ ListItem suitable for ListScreen display.
144
+
145
+ Example:
146
+ >>> container = ContainerInfo(
147
+ ... id="abc123",
148
+ ... name="scc-main",
149
+ ... status="Up 2 hours",
150
+ ... profile="team-a",
151
+ ... workspace="/home/user/project",
152
+ ... )
153
+ >>> item = format_container(container)
154
+ >>> item.label
155
+ 'scc-main'
156
+ """
157
+ # Build description parts
158
+ desc_parts: list[str] = []
159
+
160
+ if container.profile:
161
+ desc_parts.append(container.profile)
162
+
163
+ if container.workspace:
164
+ # Show just the workspace name (last path component)
165
+ workspace_name = container.workspace.split("/")[-1]
166
+ desc_parts.append(workspace_name)
167
+
168
+ if container.status:
169
+ # Simplify status (e.g., "Up 2 hours" -> "Up 2h")
170
+ status_short = _shorten_docker_status(container.status)
171
+ desc_parts.append(status_short)
172
+
173
+ # Determine if container is running
174
+ is_running = container.status.startswith("Up") if container.status else False
175
+
176
+ return ListItem(
177
+ value=container,
178
+ label=container.name,
179
+ description=" ".join(desc_parts),
180
+ metadata={
181
+ "running": "yes" if is_running else "no",
182
+ "id": container.id[:12], # Short container ID
183
+ },
184
+ )
185
+
186
+
187
+ def format_session(session: dict[str, Any]) -> ListItem[dict[str, Any]]:
188
+ """Format a session dict for display in a picker.
189
+
190
+ Args:
191
+ session: Session dictionary with name, team, branch, etc.
192
+
193
+ Returns:
194
+ ListItem suitable for ListScreen display.
195
+
196
+ Example:
197
+ >>> session = {
198
+ ... "name": "project-feature",
199
+ ... "team": "platform",
200
+ ... "branch": "feature/auth",
201
+ ... "last_used": "2 hours ago",
202
+ ... }
203
+ >>> item = format_session(session)
204
+ >>> item.label
205
+ 'project-feature'
206
+ """
207
+ name = session.get("name", "Unnamed")
208
+
209
+ # Build description parts
210
+ desc_parts: list[str] = []
211
+
212
+ if session.get("team"):
213
+ desc_parts.append(str(session["team"]))
214
+
215
+ if session.get("branch"):
216
+ desc_parts.append(str(session["branch"]))
217
+
218
+ if session.get("last_used"):
219
+ desc_parts.append(str(session["last_used"]))
220
+
221
+ # Check for governance warnings (e.g., expiring exceptions)
222
+ governance_status: str | None = None
223
+ if session.get("has_exception_warning"):
224
+ governance_status = "warning"
225
+
226
+ return ListItem(
227
+ value=session,
228
+ label=name,
229
+ description=" ".join(desc_parts),
230
+ governance_status=governance_status,
231
+ )
232
+
233
+
234
+ def format_worktree(worktree: WorktreeInfo) -> ListItem[WorktreeInfo]:
235
+ """Format a worktree for display in a picker or list.
236
+
237
+ Args:
238
+ worktree: Worktree information from Git.
239
+
240
+ Returns:
241
+ ListItem suitable for ListScreen display.
242
+
243
+ Example:
244
+ >>> from scc_cli.git import WorktreeInfo
245
+ >>> wt = WorktreeInfo(
246
+ ... path="/home/user/project-feature",
247
+ ... branch="feature/auth",
248
+ ... is_current=True,
249
+ ... has_changes=True,
250
+ ... )
251
+ >>> item = format_worktree(wt)
252
+ >>> item.label
253
+ '✓ project-feature'
254
+ """
255
+ from pathlib import Path
256
+
257
+ # Use just the directory name for the label
258
+ dir_name = Path(worktree.path).name
259
+
260
+ # Build label with current indicator
261
+ label = f"{Indicators.get('PASS')} {dir_name}" if worktree.is_current else dir_name
262
+
263
+ # Build description parts
264
+ desc_parts: list[str] = []
265
+
266
+ if worktree.branch:
267
+ # Use display-friendly name (strip SCC prefix)
268
+ desc_parts.append(get_display_branch(worktree.branch))
269
+
270
+ if worktree.has_changes:
271
+ desc_parts.append("*modified")
272
+
273
+ if worktree.is_current:
274
+ desc_parts.append("(current)")
275
+
276
+ return ListItem(
277
+ value=worktree,
278
+ label=label,
279
+ description=" ".join(desc_parts),
280
+ metadata={
281
+ "path": worktree.path,
282
+ "current": "yes" if worktree.is_current else "no",
283
+ },
284
+ )
285
+
286
+
287
+ def format_context(
288
+ context: WorkContext,
289
+ *,
290
+ is_running: bool | None = None,
291
+ is_current_branch: bool | None = None,
292
+ ) -> ListItem[WorkContext]:
293
+ """Format a work context for display in a picker.
294
+
295
+ Shows the context's display_label (team · repo · worktree) with
296
+ pinned indicator, status indicator, current branch indicator, and
297
+ relative time since last used.
298
+
299
+ Args:
300
+ context: Work context to format.
301
+ is_running: Whether the context's container is running.
302
+ True = show 🟢 (running), False = show ⚫ (stopped), None = no indicator.
303
+ is_current_branch: Whether this context matches the current git branch.
304
+ True = show ★ indicator, False/None = no indicator.
305
+
306
+ Returns:
307
+ ListItem suitable for ListScreen display.
308
+
309
+ Example:
310
+ >>> from scc_cli.contexts import WorkContext
311
+ >>> from pathlib import Path
312
+ >>> ctx = WorkContext(
313
+ ... team="platform",
314
+ ... repo_root=Path("/code/api"),
315
+ ... worktree_path=Path("/code/api"),
316
+ ... worktree_name="main",
317
+ ... pinned=True,
318
+ ... )
319
+ >>> item = format_context(ctx)
320
+ >>> item.label
321
+ '📌 platform · api · main'
322
+ >>> item = format_context(ctx, is_running=True)
323
+ >>> '🟢' in item.label
324
+ True
325
+ >>> item = format_context(ctx, is_current_branch=True)
326
+ >>> '★' in item.label
327
+ True
328
+ """
329
+ # Build label parts
330
+ parts: list[str] = []
331
+
332
+ # Add pinned indicator
333
+ if context.pinned:
334
+ parts.append("📌")
335
+
336
+ # Add current branch indicator (matches CWD branch)
337
+ if is_current_branch is True:
338
+ parts.append("★")
339
+
340
+ # Add status indicator (running/stopped)
341
+ if is_running is True:
342
+ parts.append("🟢")
343
+ elif is_running is False:
344
+ parts.append("⚫")
345
+
346
+ # Add display label
347
+ parts.append(context.display_label)
348
+
349
+ label = " ".join(parts)
350
+
351
+ # Build description parts
352
+ desc_parts: list[str] = []
353
+
354
+ # Add relative time since last used
355
+ relative_time = _format_relative_time(context.last_used)
356
+ if relative_time:
357
+ desc_parts.append(relative_time)
358
+
359
+ # Add session info if available
360
+ if context.last_session_id:
361
+ desc_parts.append(f"session: {context.last_session_id}")
362
+
363
+ return ListItem(
364
+ value=context,
365
+ label=label,
366
+ description=" ".join(desc_parts),
367
+ metadata={
368
+ "team": context.team or "", # Empty string for standalone mode (no team)
369
+ "repo": context.repo_name,
370
+ "worktree": context.worktree_name,
371
+ "path": str(context.worktree_path),
372
+ "pinned": "yes" if context.pinned else "no",
373
+ "running": "yes" if is_running else "no" if is_running is False else "",
374
+ "current_branch": (
375
+ "yes" if is_current_branch else "no" if is_current_branch is False else ""
376
+ ),
377
+ },
378
+ )
379
+
380
+
381
+ def _format_relative_time(iso_timestamp: str) -> str:
382
+ """Format an ISO timestamp as relative time (e.g., '2 hours ago').
383
+
384
+ Args:
385
+ iso_timestamp: ISO 8601 timestamp string.
386
+
387
+ Returns:
388
+ Human-readable relative time string, or empty if parsing fails.
389
+ """
390
+ try:
391
+ # Parse ISO format, handling Z suffix
392
+ timestamp = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
393
+ now = datetime.now(timezone.utc)
394
+ delta = now - timestamp
395
+
396
+ seconds = int(delta.total_seconds())
397
+ if seconds < 0:
398
+ return ""
399
+ if seconds < 60:
400
+ return "just now"
401
+ if seconds < 3600:
402
+ minutes = seconds // 60
403
+ return f"{minutes}m ago"
404
+ if seconds < 86400:
405
+ hours = seconds // 3600
406
+ return f"{hours}h ago"
407
+ if seconds < 604800:
408
+ days = seconds // 86400
409
+ return f"{days}d ago"
410
+ weeks = seconds // 604800
411
+ return f"{weeks}w ago"
412
+ except (ValueError, TypeError):
413
+ return ""
414
+
415
+
416
+ def _shorten_docker_status(status: str) -> str:
417
+ """Shorten Docker status strings for compact display.
418
+
419
+ Converts verbose time units to abbreviations:
420
+ - "Up 2 hours" -> "Up 2h"
421
+ - "Exited (0) 5 minutes ago" -> "Exited 5m ago"
422
+
423
+ Args:
424
+ status: Full Docker status string.
425
+
426
+ Returns:
427
+ Shortened status string.
428
+ """
429
+ result = status
430
+ replacements = [
431
+ (" hours", "h"),
432
+ (" hour", "h"),
433
+ (" minutes", "m"),
434
+ (" minute", "m"),
435
+ (" seconds", "s"),
436
+ (" second", "s"),
437
+ (" days", "d"),
438
+ (" day", "d"),
439
+ (" weeks", "w"),
440
+ (" week", "w"),
441
+ ]
442
+ for old, new in replacements:
443
+ result = result.replace(old, new)
444
+ return result