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.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +383 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- 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"
|