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
scc_cli/setup.py ADDED
@@ -0,0 +1,582 @@
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
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
+ return config_data
177
+
178
+
179
+ # ═══════════════════════════════════════════════════════════════════════════════
180
+ # Profile Selection
181
+ # ═══════════════════════════════════════════════════════════════════════════════
182
+
183
+
184
+ def prompt_profile_selection(console: Console, org_config: dict[str, Any]) -> str | None:
185
+ """Prompt the user to select a profile from the org config.
186
+
187
+ Args:
188
+ console: Rich console for output
189
+ org_config: Organization config with profiles
190
+
191
+ Returns:
192
+ Selected profile name or None for no profile.
193
+ """
194
+ profiles = org_config.get("profiles", {})
195
+
196
+ if not profiles:
197
+ console.print("[dim]No profiles configured.[/dim]")
198
+ return None
199
+
200
+ console.print()
201
+ console.print("[bold cyan]Select your team profile[/bold cyan]")
202
+ console.print()
203
+
204
+ # Build selection table
205
+ table = Table(
206
+ box=box.SIMPLE,
207
+ show_header=False,
208
+ padding=(0, 2),
209
+ border_style="dim",
210
+ )
211
+ table.add_column("Option", style="yellow", width=4)
212
+ table.add_column("Profile", style="cyan", min_width=15)
213
+ table.add_column("Description", style="dim")
214
+
215
+ profile_list = list(profiles.keys())
216
+
217
+ for i, profile_name in enumerate(profile_list, 1):
218
+ profile_info = profiles[profile_name]
219
+ desc = profile_info.get("description", "")
220
+ table.add_row(f"[{i}]", profile_name, desc)
221
+
222
+ table.add_row("[0]", "none", "No profile")
223
+
224
+ console.print(table)
225
+ console.print()
226
+
227
+ # Get selection
228
+ valid_choices = [str(i) for i in range(0, len(profile_list) + 1)]
229
+ choice_str = Prompt.ask(
230
+ "[cyan]Select profile[/cyan]",
231
+ default="0" if not profile_list else "1",
232
+ choices=valid_choices,
233
+ )
234
+ choice = int(choice_str)
235
+
236
+ if choice == 0:
237
+ return None
238
+
239
+ return cast(str, profile_list[choice - 1])
240
+
241
+
242
+ # ═══════════════════════════════════════════════════════════════════════════════
243
+ # Hooks Configuration
244
+ # ═══════════════════════════════════════════════════════════════════════════════
245
+
246
+
247
+ def prompt_hooks_enablement(console: Console) -> bool:
248
+ """Prompt the user about git hooks installation.
249
+
250
+ Returns:
251
+ True if hooks should be enabled, False otherwise.
252
+ """
253
+ console.print()
254
+ console.print("[bold cyan]Git Hooks Protection[/bold cyan]")
255
+ console.print()
256
+ console.print("[dim]Install repo-local hooks to block pushes to protected branches?[/dim]")
257
+ console.print("[dim](main, master, develop, production, staging)[/dim]")
258
+ console.print()
259
+
260
+ return Confirm.ask(
261
+ "[cyan]Enable git hooks protection?[/cyan]",
262
+ default=True,
263
+ )
264
+
265
+
266
+ # ═══════════════════════════════════════════════════════════════════════════════
267
+ # Save Configuration
268
+ # ═══════════════════════════════════════════════════════════════════════════════
269
+
270
+
271
+ def save_setup_config(
272
+ console: Console,
273
+ org_url: str | None,
274
+ auth: str | None,
275
+ profile: str | None,
276
+ hooks_enabled: bool,
277
+ standalone: bool = False,
278
+ ) -> None:
279
+ """Save the setup configuration to the user config file.
280
+
281
+ Args:
282
+ console: Rich console for output
283
+ org_url: Organization config URL or None
284
+ auth: Auth spec or None
285
+ profile: Selected profile name or None
286
+ hooks_enabled: Whether git hooks are enabled
287
+ standalone: Whether running in standalone mode
288
+ """
289
+ console.print()
290
+ console.print("[dim]Saving configuration...[/dim]")
291
+
292
+ # Ensure config directory exists
293
+ config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
294
+
295
+ # Build configuration
296
+ user_config: dict[str, Any] = {
297
+ "config_version": "1.0.0",
298
+ "hooks": {"enabled": hooks_enabled},
299
+ }
300
+
301
+ if standalone:
302
+ user_config["standalone"] = True
303
+ user_config["organization_source"] = None
304
+ elif org_url:
305
+ user_config["organization_source"] = {
306
+ "url": org_url,
307
+ "auth": auth,
308
+ }
309
+ user_config["selected_profile"] = profile
310
+
311
+ # Save to config file
312
+ config.save_user_config(user_config)
313
+
314
+ console.print(f"[green]✓ Configuration saved to {config.CONFIG_FILE}[/green]")
315
+
316
+
317
+ # ═══════════════════════════════════════════════════════════════════════════════
318
+ # Setup Complete Display
319
+ # ═══════════════════════════════════════════════════════════════════════════════
320
+
321
+
322
+ def show_setup_complete(
323
+ console: Console,
324
+ org_name: str | None = None,
325
+ profile: str | None = None,
326
+ standalone: bool = False,
327
+ ) -> None:
328
+ """Display the setup completion message.
329
+
330
+ Args:
331
+ console: Rich console for output
332
+ org_name: Organization name (if connected)
333
+ profile: Selected profile name
334
+ standalone: Whether in standalone mode
335
+ """
336
+ console.print()
337
+
338
+ # Build completion info
339
+ info_lines = []
340
+ if standalone:
341
+ info_lines.append("[cyan]Mode:[/cyan] Standalone (no organization)")
342
+ elif org_name:
343
+ info_lines.append(f"[cyan]Organization:[/cyan] {org_name}")
344
+ info_lines.append(f"[cyan]Profile:[/cyan] {profile or 'none'}")
345
+
346
+ info_lines.append(f"[cyan]Config:[/cyan] {config.CONFIG_DIR}")
347
+
348
+ # Create panel
349
+ panel = Panel(
350
+ "\n".join(info_lines),
351
+ title="[bold green]✓ Setup Complete[/bold green]",
352
+ border_style="green",
353
+ padding=(1, 2),
354
+ )
355
+ console.print(panel)
356
+
357
+ # Next steps
358
+ console.print()
359
+ console.print("[bold white]Get started:[/bold white]")
360
+ console.print()
361
+ console.print(" [cyan]scc start ~/project[/cyan] [dim]Start Claude Code[/dim]")
362
+ console.print(" [cyan]scc team list[/cyan] [dim]List available teams[/dim]")
363
+ console.print(" [cyan]scc doctor[/cyan] [dim]Check system health[/dim]")
364
+ console.print()
365
+
366
+
367
+ # ═══════════════════════════════════════════════════════════════════════════════
368
+ # Main Setup Wizard
369
+ # ═══════════════════════════════════════════════════════════════════════════════
370
+
371
+
372
+ def run_setup_wizard(console: Console) -> bool:
373
+ """Run the interactive setup wizard.
374
+
375
+ Flow:
376
+ 1. Prompt if user has org config URL
377
+ 2. If yes: fetch config, handle auth, select profile
378
+ 3. If no: standalone mode
379
+ 4. Configure hooks
380
+ 5. Save config
381
+
382
+ Returns:
383
+ True if setup completed successfully.
384
+ """
385
+ # Welcome
386
+ show_welcome(console)
387
+
388
+ # Check for org config
389
+ has_org_config = prompt_has_org_config(console)
390
+
391
+ if has_org_config:
392
+ # Get org URL
393
+ org_url = prompt_org_url(console)
394
+
395
+ # Try to fetch without auth first
396
+ org_config = fetch_and_validate_org_config(console, org_url, auth=None)
397
+
398
+ # If 401, prompt for auth and retry
399
+ auth = None
400
+ if org_config is None:
401
+ auth = prompt_auth_method(console)
402
+ if auth:
403
+ org_config = fetch_and_validate_org_config(console, org_url, auth=auth)
404
+
405
+ if org_config is None:
406
+ console.print("[red]✗ Could not fetch organization config[/red]")
407
+ return False
408
+
409
+ # Profile selection
410
+ profile = prompt_profile_selection(console, org_config)
411
+
412
+ # Hooks
413
+ hooks_enabled = prompt_hooks_enablement(console)
414
+
415
+ # Save config
416
+ save_setup_config(
417
+ console,
418
+ org_url=org_url,
419
+ auth=auth,
420
+ profile=profile,
421
+ hooks_enabled=hooks_enabled,
422
+ )
423
+
424
+ # Complete
425
+ org_name = org_config.get("organization", {}).get("name")
426
+ show_setup_complete(console, org_name=org_name, profile=profile)
427
+
428
+ else:
429
+ # Standalone mode
430
+ console.print()
431
+ console.print("[dim]Setting up standalone mode (no organization config)[/dim]")
432
+
433
+ # Hooks
434
+ hooks_enabled = prompt_hooks_enablement(console)
435
+
436
+ # Save config
437
+ save_setup_config(
438
+ console,
439
+ org_url=None,
440
+ auth=None,
441
+ profile=None,
442
+ hooks_enabled=hooks_enabled,
443
+ standalone=True,
444
+ )
445
+
446
+ # Complete
447
+ show_setup_complete(console, standalone=True)
448
+
449
+ return True
450
+
451
+
452
+ # ═══════════════════════════════════════════════════════════════════════════════
453
+ # Non-Interactive Setup
454
+ # ═══════════════════════════════════════════════════════════════════════════════
455
+
456
+
457
+ def run_non_interactive_setup(
458
+ console: Console,
459
+ org_url: str | None = None,
460
+ team: str | None = None,
461
+ auth: str | None = None,
462
+ standalone: bool = False,
463
+ ) -> bool:
464
+ """Run non-interactive setup using CLI arguments.
465
+
466
+ Args:
467
+ console: Rich console for output
468
+ org_url: Organization config URL
469
+ team: Team/profile name
470
+ auth: Auth spec (env:VAR or command:CMD)
471
+ standalone: Enable standalone mode
472
+
473
+ Returns:
474
+ True if setup completed successfully.
475
+ """
476
+ if standalone:
477
+ # Standalone mode - no org config needed
478
+ save_setup_config(
479
+ console,
480
+ org_url=None,
481
+ auth=None,
482
+ profile=None,
483
+ hooks_enabled=False,
484
+ standalone=True,
485
+ )
486
+ show_setup_complete(console, standalone=True)
487
+ return True
488
+
489
+ if not org_url:
490
+ console.print("[red]✗ Organization URL required (use --org-url)[/red]")
491
+ return False
492
+
493
+ # Fetch org config
494
+ org_config = fetch_and_validate_org_config(console, org_url, auth=auth)
495
+
496
+ if org_config is None:
497
+ console.print("[red]✗ Could not fetch organization config[/red]")
498
+ return False
499
+
500
+ # Validate team if provided
501
+ if team:
502
+ profiles = org_config.get("profiles", {})
503
+ if team not in profiles:
504
+ available = ", ".join(profiles.keys())
505
+ console.print(f"[red]✗ Team '{team}' not found. Available: {available}[/red]")
506
+ return False
507
+
508
+ # Save config
509
+ save_setup_config(
510
+ console,
511
+ org_url=org_url,
512
+ auth=auth,
513
+ profile=team,
514
+ hooks_enabled=True, # Default to enabled for non-interactive
515
+ )
516
+
517
+ org_name = org_config.get("organization", {}).get("name")
518
+ show_setup_complete(console, org_name=org_name, profile=team)
519
+
520
+ return True
521
+
522
+
523
+ # ═══════════════════════════════════════════════════════════════════════════════
524
+ # Setup Detection
525
+ # ═══════════════════════════════════════════════════════════════════════════════
526
+
527
+
528
+ def is_setup_needed() -> bool:
529
+ """Check if first-run setup is needed and return the result.
530
+
531
+ Return True if:
532
+ - Config directory doesn't exist
533
+ - Config file doesn't exist
534
+ - config_version field is missing
535
+ """
536
+ if not config.CONFIG_DIR.exists():
537
+ return True
538
+
539
+ if not config.CONFIG_FILE.exists():
540
+ return True
541
+
542
+ # Check for config version
543
+ user_config = config.load_user_config()
544
+ return "config_version" not in user_config
545
+
546
+
547
+ def maybe_run_setup(console: Console) -> bool:
548
+ """Run setup if needed, otherwise return True.
549
+
550
+ Call at the start of commands that require configuration.
551
+ Return True if ready to proceed, False if setup failed.
552
+ """
553
+ if not is_setup_needed():
554
+ return True
555
+
556
+ console.print()
557
+ console.print("[dim]First-time setup detected. Let's get you started![/dim]")
558
+ console.print()
559
+
560
+ return run_setup_wizard(console)
561
+
562
+
563
+ # ═══════════════════════════════════════════════════════════════════════════════
564
+ # Configuration Reset
565
+ # ═══════════════════════════════════════════════════════════════════════════════
566
+
567
+
568
+ def reset_setup(console: Console) -> None:
569
+ """Reset setup configuration to defaults.
570
+
571
+ Use when user wants to reconfigure.
572
+ """
573
+ console.print()
574
+ console.print("[bold yellow]Resetting configuration...[/bold yellow]")
575
+
576
+ if config.CONFIG_FILE.exists():
577
+ config.CONFIG_FILE.unlink()
578
+ console.print(f" [dim]Removed {config.CONFIG_FILE}[/dim]")
579
+
580
+ console.print()
581
+ console.print("[green]✓ Configuration reset.[/green] Run [bold]scc setup[/bold] again.")
582
+ console.print()