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/output_mode.py ADDED
@@ -0,0 +1,167 @@
1
+ """
2
+ JSON output mode infrastructure.
3
+
4
+ Provide context management for JSON output mode with automatic stderr suppression.
5
+ Use ContextVar for thread-safe, async-compatible state isolation.
6
+
7
+ Usage:
8
+ from scc_cli.output_mode import json_output_mode, is_json_mode, print_json, print_human
9
+
10
+ # In command handler:
11
+ with json_output_mode():
12
+ # All stderr chatter is suppressed here
13
+ result = do_work()
14
+ print_json(build_envelope(Kind.X, data=result))
15
+
16
+ # Outside JSON mode (normal human output):
17
+ print_human("Processing...") # Only prints if not in JSON mode
18
+ """
19
+
20
+ import json
21
+ import sys
22
+ from collections.abc import Generator
23
+ from contextlib import contextmanager
24
+ from contextvars import ContextVar
25
+ from typing import Any, TextIO
26
+
27
+ from rich.console import Console
28
+
29
+ # ═══════════════════════════════════════════════════════════════════════════════
30
+ # Context Variables (Thread-safe, Async-compatible)
31
+ # ═══════════════════════════════════════════════════════════════════════════════
32
+
33
+ _json_mode: ContextVar[bool] = ContextVar("json_mode", default=False)
34
+ _json_command_mode: ContextVar[bool] = ContextVar("json_command_mode", default=False)
35
+ _pretty_mode: ContextVar[bool] = ContextVar("pretty_mode", default=False)
36
+
37
+ # Console instances for stdout and stderr
38
+ # Rich Console requires file= in constructor, not in print()
39
+ console = Console()
40
+ err_console = Console(stderr=True)
41
+
42
+
43
+ # ═══════════════════════════════════════════════════════════════════════════════
44
+ # Context Managers
45
+ # ═══════════════════════════════════════════════════════════════════════════════
46
+
47
+
48
+ @contextmanager
49
+ def json_output_mode() -> Generator[None, None, None]:
50
+ """Context manager for JSON output mode.
51
+
52
+ While active:
53
+ - is_json_mode() returns True
54
+ - print_human() becomes a no-op
55
+ - All stderr chatter should be suppressed
56
+
57
+ The context is properly reset even if an exception occurs,
58
+ thanks to ContextVar's token-based reset mechanism.
59
+ """
60
+ token = _json_mode.set(True)
61
+ try:
62
+ yield
63
+ finally:
64
+ _json_mode.reset(token)
65
+
66
+
67
+ @contextmanager
68
+ def json_command_mode() -> Generator[None, None, None]:
69
+ """Context manager for JSON command handling.
70
+
71
+ This signals that errors should be handled by json_command instead of
72
+ the generic error handler, allowing consistent JSON envelopes.
73
+ """
74
+ token = _json_command_mode.set(True)
75
+ try:
76
+ yield
77
+ finally:
78
+ _json_command_mode.reset(token)
79
+
80
+
81
+ @contextmanager
82
+ def pretty_output_mode() -> Generator[None, None, None]:
83
+ """Context manager for pretty-printed JSON output.
84
+
85
+ Enables indented JSON output. Usually combined with json_output_mode.
86
+ """
87
+ token = _pretty_mode.set(True)
88
+ try:
89
+ yield
90
+ finally:
91
+ _pretty_mode.reset(token)
92
+
93
+
94
+ # ═══════════════════════════════════════════════════════════════════════════════
95
+ # State Query Functions
96
+ # ═══════════════════════════════════════════════════════════════════════════════
97
+
98
+
99
+ def is_json_mode() -> bool:
100
+ """Check if JSON output mode is active."""
101
+ return _json_mode.get()
102
+
103
+
104
+ def is_json_command_mode() -> bool:
105
+ """Check if JSON command handling mode is active."""
106
+ return _json_command_mode.get()
107
+
108
+
109
+ def is_pretty_mode() -> bool:
110
+ """Check if pretty-print mode is active for JSON output."""
111
+ return _pretty_mode.get()
112
+
113
+
114
+ def set_pretty_mode(value: bool) -> None:
115
+ """Set pretty-print mode for JSON output.
116
+
117
+ Note: This is a direct setter that doesn't use context management.
118
+ Use pretty_output_mode() context manager when possible for proper cleanup.
119
+
120
+ Args:
121
+ value: True to enable pretty-printing, False to disable.
122
+ """
123
+ _pretty_mode.set(value)
124
+
125
+
126
+ # ═══════════════════════════════════════════════════════════════════════════════
127
+ # Output Functions
128
+ # ═══════════════════════════════════════════════════════════════════════════════
129
+
130
+
131
+ def print_human(message: str, file: TextIO | None = None, **kwargs: Any) -> None:
132
+ """Print human-readable output.
133
+
134
+ This is a no-op when JSON mode is active, ensuring clean JSON output
135
+ without any interleaved human-readable messages.
136
+
137
+ Args:
138
+ message: The message to print (Rich markup supported)
139
+ file: Output target. Use sys.stderr for warnings/errors.
140
+ Note: Rich Console requires file in constructor, not print(),
141
+ so we use a separate err_console for stderr output.
142
+ **kwargs: Additional arguments passed to console.print()
143
+ """
144
+ if not is_json_mode():
145
+ # Select appropriate console based on file parameter
146
+ target = err_console if file is sys.stderr else console
147
+ target.print(message, **kwargs)
148
+
149
+
150
+ def print_json(envelope: dict[str, Any]) -> None:
151
+ """Print JSON envelope to stdout.
152
+
153
+ Output format:
154
+ - Compact (no indentation) by default for CI/scripting efficiency
155
+ - Pretty-printed (2-space indent) when pretty mode is active
156
+
157
+ Args:
158
+ envelope: The JSON envelope to output
159
+ """
160
+ if is_pretty_mode():
161
+ # Pretty mode: indented for human readability
162
+ output = json.dumps(envelope, indent=2)
163
+ else:
164
+ # Compact mode: minimal size for CI pipelines
165
+ output = json.dumps(envelope, separators=(",", ":"))
166
+
167
+ print(output)
scc_cli/panels.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ Provide Rich panel components for consistent UI across the CLI.
3
+
4
+ Define reusable panel factories for info, warning, success, and error messages
5
+ with standardized styling and structure.
6
+
7
+ All colors are sourced from ui/theme.py design tokens for consistency.
8
+ """
9
+
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from scc_cli.theme import Borders, Colors
15
+
16
+
17
+ def create_info_panel(title: str, content: str, subtitle: str = "") -> Panel:
18
+ """Create an info panel with brand styling.
19
+
20
+ Args:
21
+ title: Panel title text.
22
+ content: Main content text.
23
+ subtitle: Optional dimmed subtitle text.
24
+
25
+ Returns:
26
+ Rich Panel with brand color border and styling.
27
+ """
28
+ body = Text()
29
+ body.append(content)
30
+ if subtitle:
31
+ body.append("\n")
32
+ body.append(subtitle, style=Colors.SECONDARY)
33
+ return Panel(
34
+ body,
35
+ title=f"[{Colors.BRAND_BOLD}]{title}[/{Colors.BRAND_BOLD}]",
36
+ border_style=Borders.PANEL_INFO,
37
+ padding=(0, 1),
38
+ )
39
+
40
+
41
+ def create_warning_panel(title: str, message: str, hint: str = "") -> Panel:
42
+ """Create a warning panel with warning styling.
43
+
44
+ Args:
45
+ title: Panel title text (will have warning icon prepended).
46
+ message: Main warning message.
47
+ hint: Optional action hint text.
48
+
49
+ Returns:
50
+ Rich Panel with warning color border and styling.
51
+ """
52
+ body = Text()
53
+ body.append(message, style="bold")
54
+ if hint:
55
+ body.append("\n\n")
56
+ body.append("-> ", style=Colors.SECONDARY)
57
+ body.append(hint, style=Colors.WARNING)
58
+ return Panel(
59
+ body,
60
+ title=f"[{Colors.WARNING_BOLD}]{title}[/{Colors.WARNING_BOLD}]",
61
+ border_style=Borders.PANEL_WARNING,
62
+ padding=(0, 1),
63
+ )
64
+
65
+
66
+ def create_success_panel(title: str, items: dict[str, str]) -> Panel:
67
+ """Create a success panel with key-value summary.
68
+
69
+ Args:
70
+ title: Panel title text (will have checkmark icon prepended).
71
+ items: Dictionary of key-value pairs to display.
72
+
73
+ Returns:
74
+ Rich Panel with success color border and key-value grid.
75
+ """
76
+ grid = Table.grid(padding=(0, 2))
77
+ grid.add_column(style=Colors.SECONDARY, no_wrap=True)
78
+ grid.add_column(style=Colors.PRIMARY)
79
+
80
+ for key, value in items.items():
81
+ grid.add_row(f"{key}:", str(value))
82
+
83
+ return Panel(
84
+ grid,
85
+ title=f"[{Colors.SUCCESS_BOLD}]{title}[/{Colors.SUCCESS_BOLD}]",
86
+ border_style=Borders.PANEL_SUCCESS,
87
+ padding=(0, 1),
88
+ )
89
+
90
+
91
+ def create_error_panel(title: str, message: str, hint: str = "") -> Panel:
92
+ """Create an error panel with error styling.
93
+
94
+ Args:
95
+ title: Panel title text (will have error icon prepended).
96
+ message: Main error message.
97
+ hint: Optional fix/action hint text.
98
+
99
+ Returns:
100
+ Rich Panel with error color border and styling.
101
+ """
102
+ body = Text()
103
+ body.append(message, style="bold")
104
+ if hint:
105
+ body.append("\n\n")
106
+ body.append("-> ", style=Colors.SECONDARY)
107
+ body.append(hint, style=Colors.ERROR)
108
+ return Panel(
109
+ body,
110
+ title=f"[{Colors.ERROR_BOLD}]{title}[/{Colors.ERROR_BOLD}]",
111
+ border_style=Borders.PANEL_ERROR,
112
+ padding=(0, 1),
113
+ )
scc_cli/platform.py ADDED
@@ -0,0 +1,350 @@
1
+ """
2
+ Platform detection and cross-platform utilities.
3
+
4
+ Handle detection of:
5
+ - Operating system (macOS, Linux, Windows, WSL2)
6
+ - Path normalization across platforms
7
+ - Performance warnings for suboptimal configurations
8
+
9
+ WSL2 Considerations:
10
+ - Files on /mnt/c (Windows filesystem) are significantly slower
11
+ - Recommend using ~/projects inside WSL for optimal performance
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ from enum import Enum
17
+ from pathlib import Path
18
+
19
+
20
+ class Platform(Enum):
21
+ """Supported platforms."""
22
+
23
+ MACOS = "macos"
24
+ LINUX = "linux"
25
+ WINDOWS = "windows"
26
+ WSL2 = "wsl2"
27
+ UNKNOWN = "unknown"
28
+
29
+
30
+ # ═══════════════════════════════════════════════════════════════════════════════
31
+ # Platform Detection
32
+ # ═══════════════════════════════════════════════════════════════════════════════
33
+
34
+
35
+ def detect_platform() -> Platform:
36
+ """Detect the current platform.
37
+
38
+ Return the most specific platform identifier:
39
+ - WSL2 takes precedence over Linux
40
+ - macOS, Windows, Linux detected by sys.platform
41
+ """
42
+ if is_wsl2():
43
+ return Platform.WSL2
44
+
45
+ if sys.platform == "darwin":
46
+ return Platform.MACOS
47
+ elif sys.platform == "win32":
48
+ return Platform.WINDOWS
49
+ elif sys.platform.startswith("linux"):
50
+ return Platform.LINUX
51
+
52
+ return Platform.UNKNOWN
53
+
54
+
55
+ def is_wsl2() -> bool:
56
+ """Detect if running in WSL2 environment.
57
+
58
+ WSL2 has 'wsl2' in /proc/version (e.g., 'microsoft-standard-WSL2').
59
+ WSL1 only has 'Microsoft' without 'wsl2' marker.
60
+ """
61
+ if sys.platform != "linux":
62
+ return False
63
+
64
+ try:
65
+ with open("/proc/version") as f:
66
+ version_info = f.read().lower()
67
+ # WSL2 specifically contains 'wsl2' in the kernel version
68
+ return "wsl2" in version_info
69
+ except (FileNotFoundError, PermissionError, OSError):
70
+ return False
71
+
72
+
73
+ def is_wsl1() -> bool:
74
+ """Detect if running in WSL1 (legacy) environment.
75
+
76
+ WSL1 has 'Microsoft' in /proc/version but NOT 'wsl2'.
77
+ """
78
+ if sys.platform != "linux":
79
+ return False
80
+
81
+ try:
82
+ with open("/proc/version") as f:
83
+ version_info = f.read().lower()
84
+ # WSL1 has 'microsoft' but NOT 'wsl2'
85
+ return "microsoft" in version_info and "wsl2" not in version_info
86
+ except (FileNotFoundError, PermissionError, OSError):
87
+ return False
88
+
89
+
90
+ def is_macos() -> bool:
91
+ """Check if running on macOS."""
92
+ return sys.platform == "darwin"
93
+
94
+
95
+ def is_linux() -> bool:
96
+ """Check if running on native Linux (not WSL)."""
97
+ return sys.platform.startswith("linux") and not is_wsl2()
98
+
99
+
100
+ def is_windows() -> bool:
101
+ """Check if running on native Windows."""
102
+ return sys.platform == "win32"
103
+
104
+
105
+ def get_platform_name() -> str:
106
+ """Get human-readable platform name."""
107
+ platform = detect_platform()
108
+ names = {
109
+ Platform.MACOS: "macOS",
110
+ Platform.LINUX: "Linux",
111
+ Platform.WINDOWS: "Windows",
112
+ Platform.WSL2: "WSL2 (Windows Subsystem for Linux)",
113
+ Platform.UNKNOWN: "Unknown",
114
+ }
115
+ return names.get(platform, "Unknown")
116
+
117
+
118
+ # ═══════════════════════════════════════════════════════════════════════════════
119
+ # Path Operations
120
+ # ═══════════════════════════════════════════════════════════════════════════════
121
+
122
+
123
+ def is_windows_mount_path(path: Path) -> bool:
124
+ """Check if a path is on the Windows filesystem (via /mnt/c, /mnt/d, etc.).
125
+
126
+ In WSL2, paths like /mnt/c/Users/... are on the Windows filesystem
127
+ and have significantly slower I/O performance.
128
+ """
129
+ resolved = path.resolve()
130
+ path_str = str(resolved)
131
+
132
+ # Check for /mnt/<drive_letter> pattern
133
+ if path_str.startswith("/mnt/") and len(path_str) > 5:
134
+ # /mnt/c, /mnt/d, etc.
135
+ drive_letter = path_str[5]
136
+ if drive_letter.isalpha() and (len(path_str) == 6 or path_str[6] == "/"):
137
+ return True
138
+
139
+ return False
140
+
141
+
142
+ def normalize_path(path: str | Path) -> Path:
143
+ """Normalize a path for the current platform.
144
+
145
+ - Expand ~ to home directory
146
+ - Resolve to absolute path
147
+ - Handle Windows/Unix path differences
148
+ """
149
+ if isinstance(path, str):
150
+ path = Path(path)
151
+
152
+ # Expand user home directory
153
+ path = path.expanduser()
154
+
155
+ # Resolve to absolute path
156
+ path = path.resolve()
157
+
158
+ return path
159
+
160
+
161
+ def get_recommended_workspace_base() -> Path:
162
+ """Get the recommended workspace base directory for the platform.
163
+
164
+ - macOS/Linux: ~/projects
165
+ - WSL2: ~/projects (inside WSL, not /mnt/c)
166
+ - Windows: C:\\Users\\<user>\\projects
167
+ """
168
+ if is_wsl2():
169
+ # In WSL2, always use Linux filesystem for performance
170
+ return Path.home() / "projects"
171
+ elif is_windows():
172
+ return Path.home() / "projects"
173
+ else:
174
+ return Path.home() / "projects"
175
+
176
+
177
+ def check_path_performance(path: Path) -> tuple[bool, str | None]:
178
+ """
179
+ Check if a path has optimal performance characteristics.
180
+
181
+ Returns:
182
+ Tuple of (is_optimal, warning_message)
183
+ """
184
+ if not is_wsl2():
185
+ return True, None
186
+
187
+ if is_windows_mount_path(path):
188
+ return False, (
189
+ f"Path {path} is on the Windows filesystem.\n"
190
+ "File operations will be significantly slower.\n"
191
+ "Recommendation: Move to ~/projects inside WSL."
192
+ )
193
+
194
+ return True, None
195
+
196
+
197
+ # ═══════════════════════════════════════════════════════════════════════════════
198
+ # Environment Information
199
+ # ═══════════════════════════════════════════════════════════════════════════════
200
+
201
+
202
+ def get_shell() -> str:
203
+ """Get the current shell name."""
204
+ shell = os.environ.get("SHELL", "")
205
+ if shell:
206
+ return Path(shell).name
207
+ return "unknown"
208
+
209
+
210
+ def get_terminal() -> str:
211
+ """Get the current terminal emulator name."""
212
+ term = os.environ.get("TERM_PROGRAM", os.environ.get("TERMINAL", ""))
213
+ if term:
214
+ return term
215
+ return os.environ.get("TERM", "unknown")
216
+
217
+
218
+ def get_home_directory() -> Path:
219
+ """Get the user's home directory."""
220
+ return Path.home()
221
+
222
+
223
+ def supports_unicode() -> bool:
224
+ """Check if the terminal supports Unicode characters.
225
+
226
+ Return True if UTF-8 encoding is available.
227
+ """
228
+ encoding = sys.stdout.encoding
229
+ if encoding:
230
+ return encoding.lower() in ("utf-8", "utf8")
231
+
232
+ # Check LANG environment variable
233
+ lang = os.environ.get("LANG", "")
234
+ return "utf-8" in lang.lower() or "utf8" in lang.lower()
235
+
236
+
237
+ def supports_colors() -> bool:
238
+ """Check if the terminal supports ANSI colors.
239
+
240
+ Check various environment indicators.
241
+ """
242
+ # Rich handles this well, but we can do basic detection
243
+ if os.environ.get("NO_COLOR"):
244
+ return False
245
+
246
+ if os.environ.get("FORCE_COLOR"):
247
+ return True
248
+
249
+ # Check if stdout is a TTY
250
+ if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
251
+ return True
252
+
253
+ return False
254
+
255
+
256
+ def get_terminal_size() -> tuple[int, int]:
257
+ """Get terminal size (columns, rows).
258
+
259
+ Return (80, 24) as default if detection fails.
260
+ """
261
+ try:
262
+ size = os.get_terminal_size()
263
+ return size.columns, size.lines
264
+ except OSError:
265
+ return 80, 24
266
+
267
+
268
+ def is_wide_terminal(threshold: int = 110) -> bool:
269
+ """
270
+ Check if terminal is wide enough for full layout.
271
+
272
+ Args:
273
+ threshold: Minimum columns for "wide" mode (default 110)
274
+
275
+ Returns:
276
+ True if terminal width >= threshold
277
+ """
278
+ columns, _ = get_terminal_size()
279
+ return columns >= threshold
280
+
281
+
282
+ # ═══════════════════════════════════════════════════════════════════════════════
283
+ # Platform-Specific Paths
284
+ # ═══════════════════════════════════════════════════════════════════════════════
285
+
286
+
287
+ def get_config_dir() -> Path:
288
+ """Get the platform-appropriate configuration directory.
289
+
290
+ - macOS: ~/Library/Application Support/scc-cli
291
+ - Linux/WSL2: ~/.config/scc-cli
292
+ - Windows: %APPDATA%\\scc-cli
293
+ """
294
+ if is_macos():
295
+ return Path.home() / "Library" / "Application Support" / "scc-cli"
296
+ elif is_windows():
297
+ appdata = os.environ.get("APPDATA")
298
+ if appdata:
299
+ return Path(appdata) / "scc-cli"
300
+ return Path.home() / "AppData" / "Roaming" / "scc-cli"
301
+ else:
302
+ # Linux, WSL2
303
+ xdg_config = os.environ.get("XDG_CONFIG_HOME")
304
+ if xdg_config:
305
+ return Path(xdg_config) / "scc-cli"
306
+ return Path.home() / ".config" / "scc-cli"
307
+
308
+
309
+ def get_cache_dir() -> Path:
310
+ """Get the platform-appropriate cache directory.
311
+
312
+ - macOS: ~/Library/Caches/scc-cli
313
+ - Linux/WSL2: ~/.cache/scc-cli
314
+ - Windows: %LOCALAPPDATA%\\scc-cli\\cache
315
+ """
316
+ if is_macos():
317
+ return Path.home() / "Library" / "Caches" / "scc-cli"
318
+ elif is_windows():
319
+ localappdata = os.environ.get("LOCALAPPDATA")
320
+ if localappdata:
321
+ return Path(localappdata) / "scc-cli" / "cache"
322
+ return Path.home() / "AppData" / "Local" / "scc-cli" / "cache"
323
+ else:
324
+ # Linux, WSL2
325
+ xdg_cache = os.environ.get("XDG_CACHE_HOME")
326
+ if xdg_cache:
327
+ return Path(xdg_cache) / "scc-cli"
328
+ return Path.home() / ".cache" / "scc-cli"
329
+
330
+
331
+ def get_data_dir() -> Path:
332
+ """Get the platform-appropriate data directory.
333
+
334
+ - macOS: ~/Library/Application Support/scc-cli
335
+ - Linux/WSL2: ~/.local/share/scc-cli
336
+ - Windows: %LOCALAPPDATA%\\scc-cli\\data
337
+ """
338
+ if is_macos():
339
+ return Path.home() / "Library" / "Application Support" / "scc-cli"
340
+ elif is_windows():
341
+ localappdata = os.environ.get("LOCALAPPDATA")
342
+ if localappdata:
343
+ return Path(localappdata) / "scc-cli" / "data"
344
+ return Path.home() / "AppData" / "Local" / "scc-cli" / "data"
345
+ else:
346
+ # Linux, WSL2
347
+ xdg_data = os.environ.get("XDG_DATA_HOME")
348
+ if xdg_data:
349
+ return Path(xdg_data) / "scc-cli"
350
+ return Path.home() / ".local" / "share" / "scc-cli"