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
@@ -0,0 +1,369 @@
1
+ """Data loading functions for dashboard tabs.
2
+
3
+ This module contains functions to load data for each dashboard tab:
4
+ - Status: System overview (team, organization, counts)
5
+ - Containers: Docker containers managed by SCC
6
+ - Sessions: Recent Claude sessions
7
+ - Worktrees: Git worktrees in current repository
8
+
9
+ Each loader function returns a TabData instance ready for display.
10
+ Loaders handle errors gracefully, returning placeholder items on failure.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from ..list_screen import ListItem
18
+ from .models import DashboardTab, TabData
19
+
20
+
21
+ def _load_status_tab_data() -> TabData:
22
+ """Load Status tab data showing system overview.
23
+
24
+ The Status tab displays:
25
+ - Current team and organization info
26
+ - Sync status with remote config
27
+ - Resource counts for quick overview
28
+
29
+ Returns:
30
+ TabData with status summary items.
31
+ """
32
+ # Import here to avoid circular imports
33
+ from ... import config, sessions
34
+ from ...docker import core as docker_core
35
+
36
+ items: list[ListItem[str]] = []
37
+
38
+ # Load current team info
39
+ try:
40
+ user_config = config.load_user_config()
41
+ team = user_config.get("selected_profile")
42
+ org_source = user_config.get("organization_source")
43
+
44
+ if team:
45
+ items.append(
46
+ ListItem(
47
+ value="team",
48
+ label="Team",
49
+ description=str(team),
50
+ )
51
+ )
52
+ else:
53
+ items.append(
54
+ ListItem(
55
+ value="team",
56
+ label="Team",
57
+ description="No team selected",
58
+ )
59
+ )
60
+
61
+ # Organization/sync status
62
+ if org_source and isinstance(org_source, dict):
63
+ org_url = org_source.get("url", "")
64
+ if org_url:
65
+ # Extract domain for display
66
+ domain = org_url.replace("https://", "").replace("http://", "").split("/")[0]
67
+ items.append(
68
+ ListItem(
69
+ value="organization",
70
+ label="Organization",
71
+ description=domain,
72
+ )
73
+ )
74
+ elif user_config.get("standalone"):
75
+ items.append(
76
+ ListItem(
77
+ value="organization",
78
+ label="Mode",
79
+ description="Standalone (no remote config)",
80
+ )
81
+ )
82
+
83
+ except Exception:
84
+ items.append(
85
+ ListItem(
86
+ value="config_error",
87
+ label="Configuration",
88
+ description="Error loading config",
89
+ )
90
+ )
91
+
92
+ # Load container count
93
+ try:
94
+ containers = docker_core.list_scc_containers()
95
+ running = sum(1 for c in containers if "Up" in c.status)
96
+ total = len(containers)
97
+ items.append(
98
+ ListItem(
99
+ value="containers",
100
+ label="Containers",
101
+ description=f"{running} running, {total} total",
102
+ )
103
+ )
104
+ except Exception:
105
+ items.append(
106
+ ListItem(
107
+ value="containers",
108
+ label="Containers",
109
+ description="Unable to query Docker",
110
+ )
111
+ )
112
+
113
+ # Load session count
114
+ try:
115
+ recent_sessions = sessions.list_recent(limit=100)
116
+ session_count = len(recent_sessions)
117
+ items.append(
118
+ ListItem(
119
+ value="sessions",
120
+ label="Sessions",
121
+ description=f"{session_count} recorded",
122
+ )
123
+ )
124
+ except Exception:
125
+ items.append(
126
+ ListItem(
127
+ value="sessions",
128
+ label="Sessions",
129
+ description="Error loading sessions",
130
+ )
131
+ )
132
+
133
+ return TabData(
134
+ tab=DashboardTab.STATUS,
135
+ title="Status",
136
+ items=items,
137
+ count_active=len(items),
138
+ count_total=len(items),
139
+ )
140
+
141
+
142
+ def _load_containers_tab_data() -> TabData:
143
+ """Load Containers tab data showing SCC-managed containers.
144
+
145
+ Returns:
146
+ TabData with container list items.
147
+ """
148
+ from ...docker import core as docker_core
149
+
150
+ items: list[ListItem[str]] = []
151
+
152
+ try:
153
+ containers = docker_core.list_scc_containers()
154
+ running_count = 0
155
+
156
+ for container in containers:
157
+ is_running = "Up" in container.status
158
+ if is_running:
159
+ running_count += 1
160
+
161
+ # Build description from available info
162
+ desc_parts = []
163
+ if container.profile:
164
+ desc_parts.append(container.profile)
165
+ if container.workspace:
166
+ # Show just the workspace name
167
+ workspace_name = container.workspace.split("/")[-1]
168
+ desc_parts.append(workspace_name)
169
+ if container.status:
170
+ # Simplify status (e.g., "Up 2 hours" → "Up 2h")
171
+ status_short = container.status.replace(" hours", "h").replace(" hour", "h")
172
+ status_short = status_short.replace(" minutes", "m").replace(" minute", "m")
173
+ status_short = status_short.replace(" days", "d").replace(" day", "d")
174
+ desc_parts.append(status_short)
175
+
176
+ items.append(
177
+ ListItem(
178
+ value=container.id,
179
+ label=container.name,
180
+ description=" ".join(desc_parts),
181
+ )
182
+ )
183
+
184
+ if not items:
185
+ items.append(
186
+ ListItem(
187
+ value="no_containers",
188
+ label="No containers",
189
+ description="Run 'scc start' to create one",
190
+ )
191
+ )
192
+
193
+ return TabData(
194
+ tab=DashboardTab.CONTAINERS,
195
+ title="Containers",
196
+ items=items,
197
+ count_active=running_count,
198
+ count_total=len(containers),
199
+ )
200
+
201
+ except Exception:
202
+ return TabData(
203
+ tab=DashboardTab.CONTAINERS,
204
+ title="Containers",
205
+ items=[
206
+ ListItem(
207
+ value="error",
208
+ label="Error",
209
+ description="Unable to query Docker",
210
+ )
211
+ ],
212
+ count_active=0,
213
+ count_total=0,
214
+ )
215
+
216
+
217
+ def _load_sessions_tab_data() -> TabData:
218
+ """Load Sessions tab data showing recent Claude sessions.
219
+
220
+ Returns:
221
+ TabData with session list items. Each ListItem.value contains
222
+ the raw session dict for access in the details pane.
223
+ """
224
+ from ... import sessions
225
+
226
+ items: list[ListItem[dict[str, Any]]] = []
227
+
228
+ try:
229
+ recent = sessions.list_recent(limit=20)
230
+
231
+ for session in recent:
232
+ name = session.get("name", "Unnamed")
233
+ desc_parts = []
234
+
235
+ if session.get("team"):
236
+ desc_parts.append(str(session["team"]))
237
+ if session.get("branch"):
238
+ desc_parts.append(str(session["branch"]))
239
+ if session.get("last_used"):
240
+ desc_parts.append(str(session["last_used"]))
241
+
242
+ # Store full session dict for details pane access
243
+ items.append(
244
+ ListItem(
245
+ value=session,
246
+ label=name,
247
+ description=" ".join(desc_parts),
248
+ )
249
+ )
250
+
251
+ if not items:
252
+ # Placeholder with sentinel dict (startable: True enables Enter action)
253
+ items.append(
254
+ ListItem(
255
+ value={"_placeholder": "no_sessions", "_startable": True},
256
+ label="No sessions",
257
+ description="Start a session with 'scc start'",
258
+ )
259
+ )
260
+
261
+ return TabData(
262
+ tab=DashboardTab.SESSIONS,
263
+ title="Sessions",
264
+ items=items,
265
+ count_active=len(recent),
266
+ count_total=len(recent),
267
+ )
268
+
269
+ except Exception:
270
+ return TabData(
271
+ tab=DashboardTab.SESSIONS,
272
+ title="Sessions",
273
+ items=[
274
+ ListItem(
275
+ value="error",
276
+ label="Error",
277
+ description="Unable to load sessions",
278
+ )
279
+ ],
280
+ count_active=0,
281
+ count_total=0,
282
+ )
283
+
284
+
285
+ def _load_worktrees_tab_data() -> TabData:
286
+ """Load Worktrees tab data showing git worktrees.
287
+
288
+ Worktrees are loaded from the current working directory if it's a git repo.
289
+
290
+ Returns:
291
+ TabData with worktree list items.
292
+ """
293
+ import os
294
+ from pathlib import Path
295
+
296
+ from ... import git
297
+
298
+ items: list[ListItem[str]] = []
299
+
300
+ try:
301
+ cwd = Path(os.getcwd())
302
+ worktrees = git.list_worktrees(cwd)
303
+ current_count = 0
304
+
305
+ for wt in worktrees:
306
+ if wt.is_current:
307
+ current_count += 1
308
+
309
+ desc_parts = []
310
+ if wt.branch:
311
+ desc_parts.append(wt.branch)
312
+ if wt.has_changes:
313
+ desc_parts.append("*modified")
314
+ if wt.is_current:
315
+ desc_parts.append("(current)")
316
+
317
+ items.append(
318
+ ListItem(
319
+ value=wt.path,
320
+ label=Path(wt.path).name,
321
+ description=" ".join(desc_parts),
322
+ )
323
+ )
324
+
325
+ if not items:
326
+ items.append(
327
+ ListItem(
328
+ value="no_worktrees",
329
+ label="No worktrees",
330
+ description="Not in a git repository",
331
+ )
332
+ )
333
+
334
+ return TabData(
335
+ tab=DashboardTab.WORKTREES,
336
+ title="Worktrees",
337
+ items=items,
338
+ count_active=current_count,
339
+ count_total=len(worktrees),
340
+ )
341
+
342
+ except Exception:
343
+ return TabData(
344
+ tab=DashboardTab.WORKTREES,
345
+ title="Worktrees",
346
+ items=[
347
+ ListItem(
348
+ value="no_git",
349
+ label="Not available",
350
+ description="Not in a git repository",
351
+ )
352
+ ],
353
+ count_active=0,
354
+ count_total=0,
355
+ )
356
+
357
+
358
+ def _load_all_tab_data() -> dict[DashboardTab, TabData]:
359
+ """Load data for all dashboard tabs.
360
+
361
+ Returns:
362
+ Dictionary mapping each tab to its data.
363
+ """
364
+ return {
365
+ DashboardTab.STATUS: _load_status_tab_data(),
366
+ DashboardTab.CONTAINERS: _load_containers_tab_data(),
367
+ DashboardTab.SESSIONS: _load_sessions_tab_data(),
368
+ DashboardTab.WORKTREES: _load_worktrees_tab_data(),
369
+ }
@@ -0,0 +1,184 @@
1
+ """Data models for the dashboard module.
2
+
3
+ This module contains the core data structures used by the dashboard:
4
+ - DashboardTab: Enum for available tabs
5
+ - TabData: Content for a single tab
6
+ - DashboardState: State management for the dashboard
7
+
8
+ These models are intentionally simple dataclasses with no external dependencies
9
+ beyond the UI layer, enabling clean separation and testability.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Sequence
15
+ from dataclasses import dataclass
16
+ from enum import Enum, auto
17
+ from typing import Any
18
+
19
+ from ..list_screen import ListItem, ListState
20
+
21
+
22
+ class DashboardTab(Enum):
23
+ """Available dashboard tabs.
24
+
25
+ Each tab represents a major resource category in SCC.
26
+ Tabs are displayed in definition order (Status first, Worktrees last).
27
+ """
28
+
29
+ STATUS = auto()
30
+ CONTAINERS = auto()
31
+ SESSIONS = auto()
32
+ WORKTREES = auto()
33
+
34
+ @property
35
+ def display_name(self) -> str:
36
+ """Human-readable name for display in chrome."""
37
+ names = {
38
+ DashboardTab.STATUS: "Status",
39
+ DashboardTab.CONTAINERS: "Containers",
40
+ DashboardTab.SESSIONS: "Sessions",
41
+ DashboardTab.WORKTREES: "Worktrees",
42
+ }
43
+ return names[self]
44
+
45
+
46
+ # Ordered list for tab cycling
47
+ TAB_ORDER: tuple[DashboardTab, ...] = (
48
+ DashboardTab.STATUS,
49
+ DashboardTab.CONTAINERS,
50
+ DashboardTab.SESSIONS,
51
+ DashboardTab.WORKTREES,
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class TabData:
57
+ """Data for a single dashboard tab.
58
+
59
+ Attributes:
60
+ tab: The tab identifier.
61
+ title: Display title for the tab content area.
62
+ items: List items to display in this tab. Value type varies by tab:
63
+ - Containers/Worktrees: str (container ID or worktree name)
64
+ - Sessions: dict[str, Any] (full session data for details pane)
65
+ count_active: Number of active items (e.g., running containers).
66
+ count_total: Total number of items.
67
+ """
68
+
69
+ tab: DashboardTab
70
+ title: str
71
+ items: Sequence[ListItem[Any]]
72
+ count_active: int
73
+ count_total: int
74
+
75
+ @property
76
+ def subtitle(self) -> str:
77
+ """Generate subtitle from counts."""
78
+ if self.count_active == self.count_total:
79
+ return f"{self.count_total} total"
80
+ return f"{self.count_active} active, {self.count_total} total"
81
+
82
+
83
+ @dataclass
84
+ class DashboardState:
85
+ """State for the tabbed dashboard view.
86
+
87
+ Manages which tab is active and provides methods for tab navigation.
88
+ Each tab switch resets the list state for the new tab.
89
+
90
+ Attributes:
91
+ active_tab: Currently active tab.
92
+ tabs: Mapping from tab to its data.
93
+ list_state: Navigation state for the current tab's list.
94
+ status_message: Transient message to display (cleared on next action).
95
+ details_open: Whether the details pane is visible.
96
+ help_visible: Whether the help overlay is shown (rendered inside Live).
97
+ """
98
+
99
+ active_tab: DashboardTab
100
+ tabs: dict[DashboardTab, TabData]
101
+ list_state: ListState[str]
102
+ status_message: str | None = None
103
+ details_open: bool = False
104
+ help_visible: bool = False
105
+
106
+ @property
107
+ def current_tab_data(self) -> TabData:
108
+ """Get data for the currently active tab."""
109
+ return self.tabs[self.active_tab]
110
+
111
+ def is_placeholder_selected(self) -> bool:
112
+ """Check if the current selection is a placeholder row.
113
+
114
+ Placeholder rows represent empty states or errors (e.g., "No containers",
115
+ "Error loading sessions") and shouldn't show details.
116
+
117
+ Placeholders can be identified by:
118
+ - String value matching known placeholder names (containers, worktrees)
119
+ - Dict value with "_placeholder" key (sessions)
120
+
121
+ Returns:
122
+ True if current item is a placeholder, False otherwise.
123
+ """
124
+ current = self.list_state.current_item
125
+ if not current:
126
+ return True # No item = treat as placeholder
127
+
128
+ # Known placeholder string values from tab data loaders
129
+ placeholder_values = {
130
+ "no_containers",
131
+ "no_sessions",
132
+ "no_worktrees",
133
+ "no_git",
134
+ "error",
135
+ "config_error",
136
+ }
137
+
138
+ # Check string placeholders (must be string type first - dicts are unhashable)
139
+ if isinstance(current.value, str) and current.value in placeholder_values:
140
+ return True
141
+
142
+ # Check dict placeholders (sessions tab uses dicts)
143
+ if isinstance(current.value, dict) and "_placeholder" in current.value:
144
+ return True
145
+
146
+ return False
147
+
148
+ def switch_tab(self, tab: DashboardTab) -> DashboardState:
149
+ """Create new state with different active tab.
150
+
151
+ Resets list state (cursor, filter) for the new tab.
152
+
153
+ Args:
154
+ tab: Tab to switch to.
155
+
156
+ Returns:
157
+ New DashboardState with the specified tab active.
158
+ """
159
+ new_list_state = ListState(items=self.tabs[tab].items)
160
+ return DashboardState(
161
+ active_tab=tab,
162
+ tabs=self.tabs,
163
+ list_state=new_list_state,
164
+ )
165
+
166
+ def next_tab(self) -> DashboardState:
167
+ """Switch to the next tab (wraps around).
168
+
169
+ Returns:
170
+ New DashboardState with next tab active.
171
+ """
172
+ current_index = TAB_ORDER.index(self.active_tab)
173
+ next_index = (current_index + 1) % len(TAB_ORDER)
174
+ return self.switch_tab(TAB_ORDER[next_index])
175
+
176
+ def prev_tab(self) -> DashboardState:
177
+ """Switch to the previous tab (wraps around).
178
+
179
+ Returns:
180
+ New DashboardState with previous tab active.
181
+ """
182
+ current_index = TAB_ORDER.index(self.active_tab)
183
+ prev_index = (current_index - 1) % len(TAB_ORDER)
184
+ return self.switch_tab(TAB_ORDER[prev_index])