scc-cli 1.4.1__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 +706 -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 +1454 -0
- scc_cli/cli_org.py +1428 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +892 -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 +604 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1074 -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 +1521 -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/adapter.py +74 -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 +257 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +260 -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 +960 -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 +588 -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 +382 -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 +677 -0
- scc_cli/ui/dashboard/loaders.py +395 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +390 -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 +538 -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 +675 -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.1.dist-info/METADATA +369 -0
- scc_cli-1.4.1.dist-info/RECORD +113 -0
- scc_cli-1.4.1.dist-info/WHEEL +4 -0
- scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
scc_cli/setup.py
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Setup wizard for SCC - Sandboxed Claude CLI.
|
|
3
|
+
|
|
4
|
+
Remote organization config workflow:
|
|
5
|
+
- Prompt for org config URL (or standalone mode)
|
|
6
|
+
- Handle authentication (env:VAR, command:CMD)
|
|
7
|
+
- Team/profile selection from remote config
|
|
8
|
+
- Git hooks enablement option
|
|
9
|
+
|
|
10
|
+
Philosophy: "Get started in under 60 seconds"
|
|
11
|
+
- Minimal questions
|
|
12
|
+
- Smart defaults
|
|
13
|
+
- Clear guidance
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, cast
|
|
17
|
+
|
|
18
|
+
from rich import box
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.prompt import Confirm, Prompt
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from . import config
|
|
25
|
+
from .remote import fetch_org_config, save_to_cache
|
|
26
|
+
|
|
27
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
# Welcome Screen
|
|
29
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
WELCOME_BANNER = """
|
|
33
|
+
[cyan]╔═══════════════════════════════════════════════════════════╗[/cyan]
|
|
34
|
+
[cyan]║[/cyan] [cyan]║[/cyan]
|
|
35
|
+
[cyan]║[/cyan] [bold white]Welcome to SCC - Sandboxed Claude CLI[/bold white] [cyan]║[/cyan]
|
|
36
|
+
[cyan]║[/cyan] [cyan]║[/cyan]
|
|
37
|
+
[cyan]║[/cyan] [dim]Safe development environment for AI-assisted coding[/dim] [cyan]║[/cyan]
|
|
38
|
+
[cyan]║[/cyan] [cyan]║[/cyan]
|
|
39
|
+
[cyan]╚═══════════════════════════════════════════════════════════╝[/cyan]
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def show_welcome(console: Console) -> None:
|
|
44
|
+
"""Display the welcome banner on the console."""
|
|
45
|
+
console.print()
|
|
46
|
+
console.print(WELCOME_BANNER)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
50
|
+
# Organization Config URL
|
|
51
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def prompt_has_org_config(console: Console) -> bool:
|
|
55
|
+
"""Prompt the user to confirm if they have an organization config URL.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if user has org config URL, False for standalone mode.
|
|
59
|
+
"""
|
|
60
|
+
console.print()
|
|
61
|
+
return Confirm.ask(
|
|
62
|
+
"[cyan]Do you have an organization config URL?[/cyan]",
|
|
63
|
+
default=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def prompt_org_url(console: Console) -> str:
|
|
68
|
+
"""Prompt the user to enter the organization config URL.
|
|
69
|
+
|
|
70
|
+
Validate that URL is HTTPS. Reject HTTP URLs.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Valid HTTPS URL string.
|
|
74
|
+
"""
|
|
75
|
+
console.print()
|
|
76
|
+
console.print("[dim]Enter your organization config URL (HTTPS only)[/dim]")
|
|
77
|
+
console.print()
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
url = Prompt.ask("[cyan]Organization config URL[/cyan]")
|
|
81
|
+
|
|
82
|
+
# Validate HTTPS
|
|
83
|
+
if url.startswith("http://"):
|
|
84
|
+
console.print("[red]✗ HTTP URLs are not allowed. Please use HTTPS.[/red]")
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if not url.startswith("https://"):
|
|
88
|
+
console.print("[red]✗ URL must start with https://[/red]")
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
return url
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
95
|
+
# Authentication
|
|
96
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def prompt_auth_method(console: Console) -> str | None:
|
|
100
|
+
"""Prompt the user to select an authentication method.
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
1. Environment variable (env:VAR)
|
|
104
|
+
2. Command (command:CMD)
|
|
105
|
+
3. Skip (no auth)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Auth spec string (env:VAR or command:CMD) or None to skip.
|
|
109
|
+
"""
|
|
110
|
+
console.print()
|
|
111
|
+
console.print("[bold cyan]Authentication required[/bold cyan]")
|
|
112
|
+
console.print()
|
|
113
|
+
console.print("[dim]How would you like to provide authentication?[/dim]")
|
|
114
|
+
console.print()
|
|
115
|
+
console.print(" [yellow][1][/yellow] Environment variable (env:VAR_NAME)")
|
|
116
|
+
console.print(" [yellow][2][/yellow] Command (command:your-command)")
|
|
117
|
+
console.print(" [yellow][3][/yellow] Skip authentication")
|
|
118
|
+
console.print()
|
|
119
|
+
|
|
120
|
+
choice = Prompt.ask(
|
|
121
|
+
"[cyan]Select option[/cyan]",
|
|
122
|
+
choices=["1", "2", "3"],
|
|
123
|
+
default="1",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if choice == "1":
|
|
127
|
+
var_name = Prompt.ask("[cyan]Environment variable name[/cyan]")
|
|
128
|
+
return f"env:{var_name}"
|
|
129
|
+
|
|
130
|
+
if choice == "2":
|
|
131
|
+
command = Prompt.ask("[cyan]Command to run[/cyan]")
|
|
132
|
+
return f"command:{command}"
|
|
133
|
+
|
|
134
|
+
# Choice 3: Skip
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
139
|
+
# Remote Config Fetching
|
|
140
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def fetch_and_validate_org_config(
|
|
144
|
+
console: Console, url: str, auth: str | None
|
|
145
|
+
) -> dict[str, Any] | None:
|
|
146
|
+
"""Fetch and validate the organization config from a URL.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
console: Rich console for output
|
|
150
|
+
url: HTTPS URL to org config
|
|
151
|
+
auth: Auth spec (env:VAR, command:CMD) or None
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Organization config dict if successful, None if auth required (401).
|
|
155
|
+
"""
|
|
156
|
+
console.print()
|
|
157
|
+
console.print("[dim]Fetching organization config...[/dim]")
|
|
158
|
+
|
|
159
|
+
config_data, etag, status = fetch_org_config(url, auth=auth, etag=None)
|
|
160
|
+
|
|
161
|
+
if status == 401:
|
|
162
|
+
console.print("[yellow]⚠️ Authentication required (401)[/yellow]")
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
if status == 403:
|
|
166
|
+
console.print("[red]✗ Access denied (403)[/red]")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
if status != 200 or config_data is None:
|
|
170
|
+
console.print(f"[red]✗ Failed to fetch config (status: {status})[/red]")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
org_name = config_data.get("organization", {}).get("name", "Unknown")
|
|
174
|
+
console.print(f"[green]✓ Connected to: {org_name}[/green]")
|
|
175
|
+
|
|
176
|
+
# Save org config to cache so team commands can access it
|
|
177
|
+
# Use default TTL of 24 hours (can be overridden in config defaults)
|
|
178
|
+
ttl_hours = config_data.get("defaults", {}).get("cache_ttl_hours", 24)
|
|
179
|
+
save_to_cache(config_data, source_url=url, etag=etag, ttl_hours=ttl_hours)
|
|
180
|
+
console.print("[dim]Organization config cached locally[/dim]")
|
|
181
|
+
|
|
182
|
+
return config_data
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
186
|
+
# Profile Selection
|
|
187
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def prompt_profile_selection(console: Console, org_config: dict[str, Any]) -> str | None:
|
|
191
|
+
"""Prompt the user to select a profile from the org config.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
console: Rich console for output
|
|
195
|
+
org_config: Organization config with profiles
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Selected profile name or None for no profile.
|
|
199
|
+
"""
|
|
200
|
+
profiles = org_config.get("profiles", {})
|
|
201
|
+
|
|
202
|
+
if not profiles:
|
|
203
|
+
console.print("[dim]No profiles configured.[/dim]")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
console.print()
|
|
207
|
+
console.print("[bold cyan]Select your team profile[/bold cyan]")
|
|
208
|
+
console.print()
|
|
209
|
+
|
|
210
|
+
# Build selection table
|
|
211
|
+
table = Table(
|
|
212
|
+
box=box.SIMPLE,
|
|
213
|
+
show_header=False,
|
|
214
|
+
padding=(0, 2),
|
|
215
|
+
border_style="dim",
|
|
216
|
+
)
|
|
217
|
+
table.add_column("Option", style="yellow", width=4)
|
|
218
|
+
table.add_column("Profile", style="cyan", min_width=15)
|
|
219
|
+
table.add_column("Description", style="dim")
|
|
220
|
+
|
|
221
|
+
profile_list = list(profiles.keys())
|
|
222
|
+
|
|
223
|
+
for i, profile_name in enumerate(profile_list, 1):
|
|
224
|
+
profile_info = profiles[profile_name]
|
|
225
|
+
desc = profile_info.get("description", "")
|
|
226
|
+
table.add_row(f"[{i}]", profile_name, desc)
|
|
227
|
+
|
|
228
|
+
table.add_row("[0]", "none", "No profile")
|
|
229
|
+
|
|
230
|
+
console.print(table)
|
|
231
|
+
console.print()
|
|
232
|
+
|
|
233
|
+
# Get selection
|
|
234
|
+
valid_choices = [str(i) for i in range(0, len(profile_list) + 1)]
|
|
235
|
+
choice_str = Prompt.ask(
|
|
236
|
+
"[cyan]Select profile[/cyan]",
|
|
237
|
+
default="0" if not profile_list else "1",
|
|
238
|
+
choices=valid_choices,
|
|
239
|
+
)
|
|
240
|
+
choice = int(choice_str)
|
|
241
|
+
|
|
242
|
+
if choice == 0:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
return cast(str, profile_list[choice - 1])
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
249
|
+
# Hooks Configuration
|
|
250
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def prompt_hooks_enablement(console: Console) -> bool:
|
|
254
|
+
"""Prompt the user about git hooks installation.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if hooks should be enabled, False otherwise.
|
|
258
|
+
"""
|
|
259
|
+
console.print()
|
|
260
|
+
console.print("[bold cyan]Git Hooks Protection[/bold cyan]")
|
|
261
|
+
console.print()
|
|
262
|
+
console.print("[dim]Install repo-local hooks to block pushes to protected branches?[/dim]")
|
|
263
|
+
console.print("[dim](main, master, develop, production, staging)[/dim]")
|
|
264
|
+
console.print()
|
|
265
|
+
|
|
266
|
+
return Confirm.ask(
|
|
267
|
+
"[cyan]Enable git hooks protection?[/cyan]",
|
|
268
|
+
default=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
273
|
+
# Save Configuration
|
|
274
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def save_setup_config(
|
|
278
|
+
console: Console,
|
|
279
|
+
org_url: str | None,
|
|
280
|
+
auth: str | None,
|
|
281
|
+
profile: str | None,
|
|
282
|
+
hooks_enabled: bool,
|
|
283
|
+
standalone: bool = False,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Save the setup configuration to the user config file.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
console: Rich console for output
|
|
289
|
+
org_url: Organization config URL or None
|
|
290
|
+
auth: Auth spec or None
|
|
291
|
+
profile: Selected profile name or None
|
|
292
|
+
hooks_enabled: Whether git hooks are enabled
|
|
293
|
+
standalone: Whether running in standalone mode
|
|
294
|
+
"""
|
|
295
|
+
console.print()
|
|
296
|
+
console.print("[dim]Saving configuration...[/dim]")
|
|
297
|
+
|
|
298
|
+
# Ensure config directory exists
|
|
299
|
+
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
|
|
301
|
+
# Build configuration
|
|
302
|
+
user_config: dict[str, Any] = {
|
|
303
|
+
"config_version": "1.0.0",
|
|
304
|
+
"hooks": {"enabled": hooks_enabled},
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if standalone:
|
|
308
|
+
user_config["standalone"] = True
|
|
309
|
+
user_config["organization_source"] = None
|
|
310
|
+
elif org_url:
|
|
311
|
+
user_config["organization_source"] = {
|
|
312
|
+
"url": org_url,
|
|
313
|
+
"auth": auth,
|
|
314
|
+
}
|
|
315
|
+
user_config["selected_profile"] = profile
|
|
316
|
+
|
|
317
|
+
# Save to config file
|
|
318
|
+
config.save_user_config(user_config)
|
|
319
|
+
|
|
320
|
+
console.print(f"[green]✓ Configuration saved to {config.CONFIG_FILE}[/green]")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
324
|
+
# Setup Complete Display
|
|
325
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def show_setup_complete(
|
|
329
|
+
console: Console,
|
|
330
|
+
org_name: str | None = None,
|
|
331
|
+
profile: str | None = None,
|
|
332
|
+
standalone: bool = False,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Display the setup completion message.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
console: Rich console for output
|
|
338
|
+
org_name: Organization name (if connected)
|
|
339
|
+
profile: Selected profile name
|
|
340
|
+
standalone: Whether in standalone mode
|
|
341
|
+
"""
|
|
342
|
+
console.print()
|
|
343
|
+
|
|
344
|
+
# Build completion info
|
|
345
|
+
info_lines = []
|
|
346
|
+
if standalone:
|
|
347
|
+
info_lines.append("[cyan]Mode:[/cyan] Standalone (no organization)")
|
|
348
|
+
elif org_name:
|
|
349
|
+
info_lines.append(f"[cyan]Organization:[/cyan] {org_name}")
|
|
350
|
+
info_lines.append(f"[cyan]Profile:[/cyan] {profile or 'none'}")
|
|
351
|
+
|
|
352
|
+
info_lines.append(f"[cyan]Config:[/cyan] {config.CONFIG_DIR}")
|
|
353
|
+
|
|
354
|
+
# Create panel
|
|
355
|
+
panel = Panel(
|
|
356
|
+
"\n".join(info_lines),
|
|
357
|
+
title="[bold green]✓ Setup Complete[/bold green]",
|
|
358
|
+
border_style="green",
|
|
359
|
+
padding=(1, 2),
|
|
360
|
+
)
|
|
361
|
+
console.print(panel)
|
|
362
|
+
|
|
363
|
+
# Next steps
|
|
364
|
+
console.print()
|
|
365
|
+
console.print("[bold white]Get started:[/bold white]")
|
|
366
|
+
console.print()
|
|
367
|
+
console.print(" [cyan]scc start ~/project[/cyan] [dim]Start Claude Code[/dim]")
|
|
368
|
+
console.print(" [cyan]scc team list[/cyan] [dim]List available teams[/dim]")
|
|
369
|
+
console.print(" [cyan]scc doctor[/cyan] [dim]Check system health[/dim]")
|
|
370
|
+
console.print()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
374
|
+
# Main Setup Wizard
|
|
375
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def run_setup_wizard(console: Console) -> bool:
|
|
379
|
+
"""Run the interactive setup wizard.
|
|
380
|
+
|
|
381
|
+
Flow:
|
|
382
|
+
1. Prompt if user has org config URL
|
|
383
|
+
2. If yes: fetch config, handle auth, select profile
|
|
384
|
+
3. If no: standalone mode
|
|
385
|
+
4. Configure hooks
|
|
386
|
+
5. Save config
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
True if setup completed successfully.
|
|
390
|
+
"""
|
|
391
|
+
# Welcome
|
|
392
|
+
show_welcome(console)
|
|
393
|
+
|
|
394
|
+
# Check for org config
|
|
395
|
+
has_org_config = prompt_has_org_config(console)
|
|
396
|
+
|
|
397
|
+
if has_org_config:
|
|
398
|
+
# Get org URL
|
|
399
|
+
org_url = prompt_org_url(console)
|
|
400
|
+
|
|
401
|
+
# Try to fetch without auth first
|
|
402
|
+
org_config = fetch_and_validate_org_config(console, org_url, auth=None)
|
|
403
|
+
|
|
404
|
+
# If 401, prompt for auth and retry
|
|
405
|
+
auth = None
|
|
406
|
+
if org_config is None:
|
|
407
|
+
auth = prompt_auth_method(console)
|
|
408
|
+
if auth:
|
|
409
|
+
org_config = fetch_and_validate_org_config(console, org_url, auth=auth)
|
|
410
|
+
|
|
411
|
+
if org_config is None:
|
|
412
|
+
console.print("[red]✗ Could not fetch organization config[/red]")
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
# Profile selection
|
|
416
|
+
profile = prompt_profile_selection(console, org_config)
|
|
417
|
+
|
|
418
|
+
# Hooks
|
|
419
|
+
hooks_enabled = prompt_hooks_enablement(console)
|
|
420
|
+
|
|
421
|
+
# Save config
|
|
422
|
+
save_setup_config(
|
|
423
|
+
console,
|
|
424
|
+
org_url=org_url,
|
|
425
|
+
auth=auth,
|
|
426
|
+
profile=profile,
|
|
427
|
+
hooks_enabled=hooks_enabled,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Complete
|
|
431
|
+
org_name = org_config.get("organization", {}).get("name")
|
|
432
|
+
show_setup_complete(console, org_name=org_name, profile=profile)
|
|
433
|
+
|
|
434
|
+
else:
|
|
435
|
+
# Standalone mode
|
|
436
|
+
console.print()
|
|
437
|
+
console.print("[dim]Setting up standalone mode (no organization config)[/dim]")
|
|
438
|
+
|
|
439
|
+
# Hooks
|
|
440
|
+
hooks_enabled = prompt_hooks_enablement(console)
|
|
441
|
+
|
|
442
|
+
# Save config
|
|
443
|
+
save_setup_config(
|
|
444
|
+
console,
|
|
445
|
+
org_url=None,
|
|
446
|
+
auth=None,
|
|
447
|
+
profile=None,
|
|
448
|
+
hooks_enabled=hooks_enabled,
|
|
449
|
+
standalone=True,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Complete
|
|
453
|
+
show_setup_complete(console, standalone=True)
|
|
454
|
+
|
|
455
|
+
return True
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
459
|
+
# Non-Interactive Setup
|
|
460
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def run_non_interactive_setup(
|
|
464
|
+
console: Console,
|
|
465
|
+
org_url: str | None = None,
|
|
466
|
+
team: str | None = None,
|
|
467
|
+
auth: str | None = None,
|
|
468
|
+
standalone: bool = False,
|
|
469
|
+
) -> bool:
|
|
470
|
+
"""Run non-interactive setup using CLI arguments.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
console: Rich console for output
|
|
474
|
+
org_url: Organization config URL
|
|
475
|
+
team: Team/profile name
|
|
476
|
+
auth: Auth spec (env:VAR or command:CMD)
|
|
477
|
+
standalone: Enable standalone mode
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if setup completed successfully.
|
|
481
|
+
"""
|
|
482
|
+
if standalone:
|
|
483
|
+
# Standalone mode - no org config needed
|
|
484
|
+
save_setup_config(
|
|
485
|
+
console,
|
|
486
|
+
org_url=None,
|
|
487
|
+
auth=None,
|
|
488
|
+
profile=None,
|
|
489
|
+
hooks_enabled=False,
|
|
490
|
+
standalone=True,
|
|
491
|
+
)
|
|
492
|
+
show_setup_complete(console, standalone=True)
|
|
493
|
+
return True
|
|
494
|
+
|
|
495
|
+
if not org_url:
|
|
496
|
+
console.print("[red]✗ Organization URL required (use --org-url)[/red]")
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
# Fetch org config
|
|
500
|
+
org_config = fetch_and_validate_org_config(console, org_url, auth=auth)
|
|
501
|
+
|
|
502
|
+
if org_config is None:
|
|
503
|
+
console.print("[red]✗ Could not fetch organization config[/red]")
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
# Validate team if provided
|
|
507
|
+
if team:
|
|
508
|
+
profiles = org_config.get("profiles", {})
|
|
509
|
+
if team not in profiles:
|
|
510
|
+
available = ", ".join(profiles.keys())
|
|
511
|
+
console.print(f"[red]✗ Team '{team}' not found. Available: {available}[/red]")
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
# Save config
|
|
515
|
+
save_setup_config(
|
|
516
|
+
console,
|
|
517
|
+
org_url=org_url,
|
|
518
|
+
auth=auth,
|
|
519
|
+
profile=team,
|
|
520
|
+
hooks_enabled=True, # Default to enabled for non-interactive
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
org_name = org_config.get("organization", {}).get("name")
|
|
524
|
+
show_setup_complete(console, org_name=org_name, profile=team)
|
|
525
|
+
|
|
526
|
+
return True
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
530
|
+
# Setup Detection
|
|
531
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def is_setup_needed() -> bool:
|
|
535
|
+
"""Check if first-run setup is needed and return the result.
|
|
536
|
+
|
|
537
|
+
Return True if:
|
|
538
|
+
- Config directory doesn't exist
|
|
539
|
+
- Config file doesn't exist
|
|
540
|
+
- config_version field is missing
|
|
541
|
+
"""
|
|
542
|
+
if not config.CONFIG_DIR.exists():
|
|
543
|
+
return True
|
|
544
|
+
|
|
545
|
+
if not config.CONFIG_FILE.exists():
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
# Check for config version
|
|
549
|
+
user_config = config.load_user_config()
|
|
550
|
+
return "config_version" not in user_config
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def maybe_run_setup(console: Console) -> bool:
|
|
554
|
+
"""Run setup if needed, otherwise return True.
|
|
555
|
+
|
|
556
|
+
Call at the start of commands that require configuration.
|
|
557
|
+
Return True if ready to proceed, False if setup failed.
|
|
558
|
+
"""
|
|
559
|
+
if not is_setup_needed():
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
console.print()
|
|
563
|
+
console.print("[dim]First-time setup detected. Let's get you started![/dim]")
|
|
564
|
+
console.print()
|
|
565
|
+
|
|
566
|
+
return run_setup_wizard(console)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
570
|
+
# Configuration Reset
|
|
571
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def reset_setup(console: Console) -> None:
|
|
575
|
+
"""Reset setup configuration to defaults.
|
|
576
|
+
|
|
577
|
+
Use when user wants to reconfigure.
|
|
578
|
+
"""
|
|
579
|
+
console.print()
|
|
580
|
+
console.print("[bold yellow]Resetting configuration...[/bold yellow]")
|
|
581
|
+
|
|
582
|
+
if config.CONFIG_FILE.exists():
|
|
583
|
+
config.CONFIG_FILE.unlink()
|
|
584
|
+
console.print(f" [dim]Removed {config.CONFIG_FILE}[/dim]")
|
|
585
|
+
|
|
586
|
+
console.print()
|
|
587
|
+
console.print("[green]✓ Configuration reset.[/green] Run [bold]scc setup[/bold] again.")
|
|
588
|
+
console.print()
|