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.

Files changed (113) 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 +706 -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 +1454 -0
  16. scc_cli/cli_org.py +1428 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +892 -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 +604 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1074 -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 +1521 -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/adapter.py +74 -0
  46. scc_cli/marketplace/compute.py +377 -0
  47. scc_cli/marketplace/constants.py +87 -0
  48. scc_cli/marketplace/managed.py +135 -0
  49. scc_cli/marketplace/materialize.py +723 -0
  50. scc_cli/marketplace/normalize.py +548 -0
  51. scc_cli/marketplace/render.py +257 -0
  52. scc_cli/marketplace/resolve.py +459 -0
  53. scc_cli/marketplace/schema.py +506 -0
  54. scc_cli/marketplace/sync.py +260 -0
  55. scc_cli/marketplace/team_cache.py +195 -0
  56. scc_cli/marketplace/team_fetch.py +688 -0
  57. scc_cli/marketplace/trust.py +244 -0
  58. scc_cli/models/__init__.py +41 -0
  59. scc_cli/models/exceptions.py +273 -0
  60. scc_cli/models/plugin_audit.py +434 -0
  61. scc_cli/org_templates.py +269 -0
  62. scc_cli/output_mode.py +167 -0
  63. scc_cli/panels.py +113 -0
  64. scc_cli/platform.py +350 -0
  65. scc_cli/profiles.py +960 -0
  66. scc_cli/remote.py +443 -0
  67. scc_cli/schemas/__init__.py +1 -0
  68. scc_cli/schemas/org-v1.schema.json +456 -0
  69. scc_cli/schemas/team-config.v1.schema.json +163 -0
  70. scc_cli/sessions.py +425 -0
  71. scc_cli/setup.py +588 -0
  72. scc_cli/source_resolver.py +470 -0
  73. scc_cli/stats.py +378 -0
  74. scc_cli/stores/__init__.py +13 -0
  75. scc_cli/stores/exception_store.py +251 -0
  76. scc_cli/subprocess_utils.py +88 -0
  77. scc_cli/teams.py +382 -0
  78. scc_cli/templates/__init__.py +2 -0
  79. scc_cli/templates/org/__init__.py +0 -0
  80. scc_cli/templates/org/minimal.json +19 -0
  81. scc_cli/templates/org/reference.json +74 -0
  82. scc_cli/templates/org/strict.json +38 -0
  83. scc_cli/templates/org/teams.json +42 -0
  84. scc_cli/templates/statusline.sh +75 -0
  85. scc_cli/theme.py +348 -0
  86. scc_cli/ui/__init__.py +124 -0
  87. scc_cli/ui/branding.py +68 -0
  88. scc_cli/ui/chrome.py +395 -0
  89. scc_cli/ui/dashboard/__init__.py +62 -0
  90. scc_cli/ui/dashboard/_dashboard.py +677 -0
  91. scc_cli/ui/dashboard/loaders.py +395 -0
  92. scc_cli/ui/dashboard/models.py +184 -0
  93. scc_cli/ui/dashboard/orchestrator.py +390 -0
  94. scc_cli/ui/formatters.py +443 -0
  95. scc_cli/ui/gate.py +350 -0
  96. scc_cli/ui/help.py +157 -0
  97. scc_cli/ui/keys.py +538 -0
  98. scc_cli/ui/list_screen.py +431 -0
  99. scc_cli/ui/picker.py +700 -0
  100. scc_cli/ui/prompts.py +200 -0
  101. scc_cli/ui/wizard.py +675 -0
  102. scc_cli/update.py +680 -0
  103. scc_cli/utils/__init__.py +39 -0
  104. scc_cli/utils/fixit.py +264 -0
  105. scc_cli/utils/fuzzy.py +124 -0
  106. scc_cli/utils/locks.py +101 -0
  107. scc_cli/utils/ttl.py +376 -0
  108. scc_cli/validate.py +455 -0
  109. scc_cli-1.4.1.dist-info/METADATA +369 -0
  110. scc_cli-1.4.1.dist-info/RECORD +113 -0
  111. scc_cli-1.4.1.dist-info/WHEEL +4 -0
  112. scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
  113. 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()