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
scc_cli/ui/prompts.py ADDED
@@ -0,0 +1,201 @@
1
+ """Simple Rich-based prompts for CLI interactions.
2
+
3
+ This module provides straightforward prompt utilities for user input that don't
4
+ require full TUI screens. For more complex interactive pickers with keyboard
5
+ navigation, see picker.py and wizard.py.
6
+
7
+ Functions:
8
+ render_error: Display an SCCError with user-friendly formatting
9
+ select_session: Interactive session selection from a list
10
+ select_team: Interactive team selection menu
11
+ prompt_custom_workspace: Prompt for custom workspace path
12
+ prompt_repo_url: Prompt for Git repository URL
13
+ """
14
+
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from rich import box
19
+ from rich.console import Console
20
+ from rich.panel import Panel
21
+ from rich.prompt import IntPrompt, Prompt
22
+ from rich.table import Table
23
+
24
+ from scc_cli.confirm import Confirm
25
+ from scc_cli.theme import Borders, Colors
26
+
27
+ if TYPE_CHECKING:
28
+ from scc_cli.core.errors import SCCError
29
+
30
+
31
+ def render_error(console: Console, error: "SCCError", debug: bool = False) -> None:
32
+ """Render an error with user-friendly formatting.
33
+
34
+ Philosophy: "One message, one action"
35
+ - Display what went wrong (user_message)
36
+ - Display what to do next (suggested_action)
37
+ - Display debug info only if --debug flag is used
38
+
39
+ Args:
40
+ console: Rich console for output.
41
+ error: The SCCError to render.
42
+ debug: Whether to show debug context.
43
+ """
44
+ lines = []
45
+
46
+ # Main error message
47
+ lines.append(f"[bold]{error.user_message}[/bold]")
48
+
49
+ # Suggested action (if available)
50
+ if error.suggested_action:
51
+ lines.append("")
52
+ lines.append(f"[{Colors.SECONDARY}]->[/{Colors.SECONDARY}] {error.suggested_action}")
53
+
54
+ # Debug context (only with --debug)
55
+ if debug and error.debug_context:
56
+ lines.append("")
57
+ lines.append(f"[{Colors.SECONDARY}]--- Debug Info ---[/{Colors.SECONDARY}]")
58
+ lines.append(f"[{Colors.SECONDARY}]{error.debug_context}[/{Colors.SECONDARY}]")
59
+ elif error.debug_context and not debug:
60
+ lines.append("")
61
+ lines.append(
62
+ f"[{Colors.SECONDARY}]Run with --debug for technical details[/{Colors.SECONDARY}]"
63
+ )
64
+
65
+ # Create panel with error styling
66
+ panel = Panel(
67
+ "\n".join(lines),
68
+ title=f"[{Colors.ERROR_BOLD}]Error[/{Colors.ERROR_BOLD}]",
69
+ border_style=Borders.PANEL_ERROR,
70
+ padding=(0, 1),
71
+ )
72
+
73
+ console.print()
74
+ console.print(panel)
75
+ console.print()
76
+
77
+
78
+ def select_session(console: Console, sessions_list: list[dict[str, Any]]) -> dict[str, Any] | None:
79
+ """Display an interactive session selection menu.
80
+
81
+ Args:
82
+ console: Rich console for output.
83
+ sessions_list: List of session dicts with 'name', 'workspace', 'last_used', etc.
84
+
85
+ Returns:
86
+ Selected session dict or None if cancelled.
87
+ """
88
+ if not sessions_list:
89
+ console.print(f"[{Colors.WARNING}]No sessions available.[/{Colors.WARNING}]")
90
+ return None
91
+
92
+ console.print(f"\n[{Colors.BRAND_BOLD}]Select a session:[/{Colors.BRAND_BOLD}]\n")
93
+
94
+ table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
95
+ table.add_column("Option", style=Colors.WARNING, width=4)
96
+ table.add_column("Name", style=Colors.BRAND)
97
+ table.add_column("Workspace", style=Colors.PRIMARY)
98
+ table.add_column("Last Used", style=Colors.SECONDARY)
99
+
100
+ for i, session in enumerate(sessions_list, 1):
101
+ name = session.get("name", "-")
102
+ workspace = session.get("workspace", "-")
103
+ last_used = session.get("last_used", "-")
104
+ table.add_row(f"[{i}]", name, workspace, last_used)
105
+
106
+ table.add_row("[0]", "<- Cancel", "", "")
107
+
108
+ console.print(table)
109
+
110
+ valid_choices = [str(i) for i in range(0, len(sessions_list) + 1)]
111
+ choice = IntPrompt.ask(
112
+ f"\n[{Colors.BRAND}]Select session[/{Colors.BRAND}]",
113
+ default=1,
114
+ choices=valid_choices,
115
+ )
116
+
117
+ if choice == 0:
118
+ return None
119
+
120
+ return sessions_list[choice - 1]
121
+
122
+
123
+ def select_team(console: Console, cfg: dict[str, Any]) -> str | None:
124
+ """Display an interactive team selection menu and return the chosen team.
125
+
126
+ Args:
127
+ console: Rich console for output.
128
+ cfg: Configuration dict containing 'profiles' key with team definitions.
129
+
130
+ Returns:
131
+ Selected team name or None if no teams available.
132
+ """
133
+ teams: dict[str, Any] = cfg.get("profiles", {})
134
+ team_list: list[str] = list(teams.keys())
135
+
136
+ if not team_list:
137
+ return None
138
+
139
+ console.print(f"\n[{Colors.BRAND_BOLD}]Select your team:[/{Colors.BRAND_BOLD}]\n")
140
+
141
+ table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
142
+ table.add_column("Option", style=Colors.WARNING, width=4)
143
+ table.add_column("Team", style=Colors.BRAND)
144
+ table.add_column("Description", style=Colors.PRIMARY)
145
+
146
+ for i, team_name in enumerate(team_list, 1):
147
+ team_info = teams[team_name]
148
+ desc = team_info.get("description", "")
149
+ table.add_row(f"[{i}]", team_name, desc)
150
+
151
+ console.print(table)
152
+
153
+ choice = IntPrompt.ask(
154
+ f"\n[{Colors.BRAND}]Select team[/{Colors.BRAND}]",
155
+ default=1,
156
+ choices=[str(i) for i in range(1, len(team_list) + 1)],
157
+ )
158
+
159
+ selected = team_list[choice - 1]
160
+ console.print(f"\n[{Colors.SUCCESS}]Selected: {selected}[/{Colors.SUCCESS}]")
161
+
162
+ return selected
163
+
164
+
165
+ def prompt_custom_workspace(console: Console) -> str | None:
166
+ """Prompt the user to enter a custom workspace path.
167
+
168
+ Args:
169
+ console: Rich console for output.
170
+
171
+ Returns:
172
+ Resolved absolute path string, or None if cancelled or path invalid.
173
+ """
174
+ path = Prompt.ask(f"\n[{Colors.BRAND}]Enter workspace path[/{Colors.BRAND}]")
175
+
176
+ if not path:
177
+ return None
178
+
179
+ expanded = Path(path).expanduser().resolve()
180
+
181
+ if not expanded.exists():
182
+ console.print(f"[{Colors.ERROR}]Path does not exist: {expanded}[/{Colors.ERROR}]")
183
+ if Confirm.ask(f"[{Colors.BRAND}]Create this directory?[/{Colors.BRAND}]", default=False):
184
+ expanded.mkdir(parents=True, exist_ok=True)
185
+ return str(expanded)
186
+ return None
187
+
188
+ return str(expanded)
189
+
190
+
191
+ def prompt_repo_url(console: Console) -> str:
192
+ """Prompt the user to enter a Git repository URL.
193
+
194
+ Args:
195
+ console: Rich console for output.
196
+
197
+ Returns:
198
+ The entered URL string (may be empty if user pressed Enter).
199
+ """
200
+ url = Prompt.ask(f"\n[{Colors.BRAND}]Repository URL (HTTPS or SSH)[/{Colors.BRAND}]")
201
+ return url
@@ -0,0 +1,116 @@
1
+ """Quick Resume gating and filtering.
2
+
3
+ Centralizes Quick Resume logic to prevent drift between entry points.
4
+
5
+ Policy (explicit):
6
+ - Show QR only if:
7
+ - Interactive allowed (TTY, not --json, not --non-interactive)
8
+ - None of --resume, --select, --fresh set
9
+ - If --interactive is set => wizard only, NO QR (force wizard bypasses QR)
10
+ - Sessions exist for WR
11
+
12
+ - QR selection list filtering:
13
+ - Filter by workspace_root == WR (only sessions for this workspace)
14
+ - AND team scoping: --team X shows only team X; standalone shows only team=None
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from scc_cli.contexts import WorkContext
24
+
25
+
26
+ def should_show_quick_resume(
27
+ *,
28
+ json_mode: bool = False,
29
+ non_interactive: bool = False,
30
+ resume: bool = False,
31
+ select: bool = False,
32
+ fresh: bool = False,
33
+ interactive_flag: bool = False,
34
+ ) -> bool:
35
+ """Determine if Quick Resume picker should be shown.
36
+
37
+ Returns False if any bypass condition is met.
38
+
39
+ Args:
40
+ json_mode: --json flag set
41
+ non_interactive: --non-interactive flag set
42
+ resume: --resume flag set
43
+ select: --select flag set
44
+ fresh: --fresh flag set
45
+ interactive_flag: --interactive flag set (forces wizard, bypasses QR)
46
+
47
+ Returns:
48
+ True if QR should be shown, False otherwise
49
+ """
50
+ # Non-interactive modes never show QR
51
+ if json_mode or non_interactive:
52
+ return False
53
+
54
+ # Explicit flags bypass QR
55
+ if resume or select or fresh:
56
+ return False
57
+
58
+ # --interactive forces wizard, bypasses QR
59
+ if interactive_flag:
60
+ return False
61
+
62
+ return True
63
+
64
+
65
+ def load_contexts_for_workspace_and_team(
66
+ workspace_root: Path,
67
+ team: str | None,
68
+ limit: int = 10,
69
+ ) -> list[WorkContext]:
70
+ """Load contexts filtered by workspace and team.
71
+
72
+ Args:
73
+ workspace_root: Only return contexts matching this workspace
74
+ team: Team filter:
75
+ - None: return only standalone contexts (team=None)
76
+ - str: return only contexts matching this team
77
+ limit: Maximum number of contexts to return
78
+
79
+ Returns:
80
+ List of WorkContext objects matching filters
81
+ """
82
+ from scc_cli.contexts import load_recent_contexts
83
+
84
+ # Load all recent contexts
85
+ all_contexts = load_recent_contexts(limit=limit * 3) # Load extra for filtering
86
+
87
+ filtered = []
88
+ for ctx in all_contexts:
89
+ # Filter by workspace
90
+ ctx_workspace = Path(ctx.repo_root) if ctx.repo_root else None
91
+ if ctx_workspace is None:
92
+ continue
93
+
94
+ # Resolve both paths for comparison
95
+ try:
96
+ if ctx_workspace.resolve() != workspace_root.resolve():
97
+ continue
98
+ except OSError:
99
+ continue
100
+
101
+ # Filter by team
102
+ if team is None:
103
+ # Standalone mode: only show contexts with no team
104
+ if ctx.team is not None:
105
+ continue
106
+ else:
107
+ # Team mode: only show contexts matching this team
108
+ if ctx.team != team:
109
+ continue
110
+
111
+ filtered.append(ctx)
112
+
113
+ if len(filtered) >= limit:
114
+ break
115
+
116
+ return filtered