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