skilllite 0.1.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.
- skilllite/__init__.py +159 -0
- skilllite/analyzer.py +391 -0
- skilllite/builtin_tools.py +240 -0
- skilllite/cli.py +217 -0
- skilllite/core/__init__.py +65 -0
- skilllite/core/executor.py +182 -0
- skilllite/core/handler.py +332 -0
- skilllite/core/loops.py +770 -0
- skilllite/core/manager.py +507 -0
- skilllite/core/metadata.py +338 -0
- skilllite/core/prompt_builder.py +321 -0
- skilllite/core/registry.py +185 -0
- skilllite/core/skill_info.py +181 -0
- skilllite/core/tool_builder.py +338 -0
- skilllite/core/tools.py +253 -0
- skilllite/mcp/__init__.py +45 -0
- skilllite/mcp/server.py +734 -0
- skilllite/quick.py +420 -0
- skilllite/sandbox/__init__.py +36 -0
- skilllite/sandbox/base.py +93 -0
- skilllite/sandbox/config.py +229 -0
- skilllite/sandbox/skillbox/__init__.py +44 -0
- skilllite/sandbox/skillbox/binary.py +421 -0
- skilllite/sandbox/skillbox/executor.py +608 -0
- skilllite/sandbox/utils.py +77 -0
- skilllite/validation.py +137 -0
- skilllite-0.1.0.dist-info/METADATA +293 -0
- skilllite-0.1.0.dist-info/RECORD +32 -0
- skilllite-0.1.0.dist-info/WHEEL +5 -0
- skilllite-0.1.0.dist-info/entry_points.txt +3 -0
- skilllite-0.1.0.dist-info/licenses/LICENSE +21 -0
- skilllite-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sandbox configuration management.
|
|
3
|
+
|
|
4
|
+
This module provides centralized configuration for the sandbox executor,
|
|
5
|
+
including default values, environment variable handling, and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Default configuration values
|
|
14
|
+
DEFAULT_EXECUTION_TIMEOUT = 120 # seconds
|
|
15
|
+
DEFAULT_MAX_MEMORY_MB = 512 # MB
|
|
16
|
+
DEFAULT_SANDBOX_LEVEL = "3" # Level 3: Sandbox isolation + static code scanning
|
|
17
|
+
DEFAULT_ALLOW_NETWORK = False
|
|
18
|
+
DEFAULT_ENABLE_SANDBOX = True
|
|
19
|
+
DEFAULT_AUTO_INSTALL = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SandboxConfig:
|
|
24
|
+
"""
|
|
25
|
+
Configuration for sandbox execution.
|
|
26
|
+
|
|
27
|
+
This class centralizes all configuration options for the sandbox executor,
|
|
28
|
+
supporting both programmatic configuration and environment variables.
|
|
29
|
+
|
|
30
|
+
Priority order (highest to lowest):
|
|
31
|
+
1. Explicit constructor arguments
|
|
32
|
+
2. Environment variables
|
|
33
|
+
3. Default values
|
|
34
|
+
|
|
35
|
+
Environment Variables:
|
|
36
|
+
SKILLBOX_BINARY_PATH: Path to the skillbox binary
|
|
37
|
+
SKILLBOX_CACHE_DIR: Directory for caching virtual environments
|
|
38
|
+
SKILLBOX_SANDBOX_LEVEL: Security level (1/2/3)
|
|
39
|
+
SKILLBOX_MAX_MEMORY_MB: Maximum memory limit in MB
|
|
40
|
+
SKILLBOX_TIMEOUT_SECS: Execution timeout in seconds
|
|
41
|
+
SKILLBOX_ALLOW_NETWORK: Allow network access (true/false/1/0)
|
|
42
|
+
SKILLBOX_ENABLE_SANDBOX: Enable sandbox protection (true/false/1/0)
|
|
43
|
+
SKILLBOX_AUTO_APPROVE: Auto-approve security prompts (true/false/1/0)
|
|
44
|
+
|
|
45
|
+
# Legacy environment variables (deprecated, use SKILLBOX_* prefix)
|
|
46
|
+
EXECUTION_TIMEOUT: Execution timeout in seconds
|
|
47
|
+
MAX_MEMORY_MB: Maximum memory limit in MB
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
binary_path: Path to the skillbox binary. If None, auto-detect.
|
|
51
|
+
cache_dir: Directory for caching virtual environments.
|
|
52
|
+
allow_network: Whether to allow network access by default.
|
|
53
|
+
enable_sandbox: Whether to enable sandbox protection.
|
|
54
|
+
execution_timeout: Skill execution timeout in seconds.
|
|
55
|
+
max_memory_mb: Maximum memory limit in MB.
|
|
56
|
+
sandbox_level: Sandbox security level (1/2/3).
|
|
57
|
+
auto_install: Automatically download and install binary if not found.
|
|
58
|
+
auto_approve: Auto-approve security prompts in Level 3.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
binary_path: Optional[str] = None
|
|
62
|
+
cache_dir: Optional[str] = None
|
|
63
|
+
allow_network: bool = field(default_factory=lambda: _parse_bool_env("SKILLBOX_ALLOW_NETWORK", DEFAULT_ALLOW_NETWORK))
|
|
64
|
+
enable_sandbox: bool = field(default_factory=lambda: _parse_bool_env("SKILLBOX_ENABLE_SANDBOX", DEFAULT_ENABLE_SANDBOX))
|
|
65
|
+
execution_timeout: int = field(default_factory=lambda: _get_timeout_from_env())
|
|
66
|
+
max_memory_mb: int = field(default_factory=lambda: _get_memory_from_env())
|
|
67
|
+
sandbox_level: str = field(default_factory=lambda: os.environ.get("SKILLBOX_SANDBOX_LEVEL", DEFAULT_SANDBOX_LEVEL))
|
|
68
|
+
auto_install: bool = field(default_factory=lambda: _parse_bool_env("SKILLBOX_AUTO_INSTALL", DEFAULT_AUTO_INSTALL))
|
|
69
|
+
auto_approve: bool = field(default_factory=lambda: _parse_bool_env("SKILLBOX_AUTO_APPROVE", False))
|
|
70
|
+
|
|
71
|
+
def __post_init__(self):
|
|
72
|
+
"""Validate configuration after initialization."""
|
|
73
|
+
self._validate()
|
|
74
|
+
|
|
75
|
+
def _validate(self) -> None:
|
|
76
|
+
"""Validate configuration values."""
|
|
77
|
+
# Validate sandbox level
|
|
78
|
+
if self.sandbox_level not in ("1", "2", "3"):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid sandbox_level '{self.sandbox_level}'. "
|
|
81
|
+
f"Must be '1', '2', or '3'."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Validate timeout
|
|
85
|
+
if self.execution_timeout <= 0:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Invalid execution_timeout {self.execution_timeout}. "
|
|
88
|
+
f"Must be a positive integer."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Validate memory limit
|
|
92
|
+
if self.max_memory_mb <= 0:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Invalid max_memory_mb {self.max_memory_mb}. "
|
|
95
|
+
f"Must be a positive integer."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_env(cls) -> "SandboxConfig":
|
|
100
|
+
"""
|
|
101
|
+
Create configuration from environment variables only.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
SandboxConfig with values from environment variables.
|
|
105
|
+
"""
|
|
106
|
+
return cls(
|
|
107
|
+
binary_path=os.environ.get("SKILLBOX_BINARY_PATH"),
|
|
108
|
+
cache_dir=os.environ.get("SKILLBOX_CACHE_DIR"),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def with_overrides(
|
|
112
|
+
self,
|
|
113
|
+
binary_path: Optional[str] = None,
|
|
114
|
+
cache_dir: Optional[str] = None,
|
|
115
|
+
allow_network: Optional[bool] = None,
|
|
116
|
+
enable_sandbox: Optional[bool] = None,
|
|
117
|
+
execution_timeout: Optional[int] = None,
|
|
118
|
+
max_memory_mb: Optional[int] = None,
|
|
119
|
+
sandbox_level: Optional[str] = None,
|
|
120
|
+
auto_install: Optional[bool] = None,
|
|
121
|
+
) -> "SandboxConfig":
|
|
122
|
+
"""
|
|
123
|
+
Create a new config with specified overrides.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
binary_path: Override binary path
|
|
127
|
+
cache_dir: Override cache directory
|
|
128
|
+
allow_network: Override network setting
|
|
129
|
+
enable_sandbox: Override sandbox setting
|
|
130
|
+
execution_timeout: Override timeout
|
|
131
|
+
max_memory_mb: Override memory limit
|
|
132
|
+
sandbox_level: Override sandbox level
|
|
133
|
+
auto_install: Override auto-install setting
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
New SandboxConfig with overrides applied.
|
|
137
|
+
"""
|
|
138
|
+
return SandboxConfig(
|
|
139
|
+
binary_path=binary_path if binary_path is not None else self.binary_path,
|
|
140
|
+
cache_dir=cache_dir if cache_dir is not None else self.cache_dir,
|
|
141
|
+
allow_network=allow_network if allow_network is not None else self.allow_network,
|
|
142
|
+
enable_sandbox=enable_sandbox if enable_sandbox is not None else self.enable_sandbox,
|
|
143
|
+
execution_timeout=execution_timeout if execution_timeout is not None else self.execution_timeout,
|
|
144
|
+
max_memory_mb=max_memory_mb if max_memory_mb is not None else self.max_memory_mb,
|
|
145
|
+
sandbox_level=sandbox_level if sandbox_level is not None else self.sandbox_level,
|
|
146
|
+
auto_install=auto_install if auto_install is not None else self.auto_install,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _parse_bool_env(key: str, default: bool) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Parse a boolean value from environment variable.
|
|
153
|
+
|
|
154
|
+
Accepts: true, false, 1, 0, yes, no (case-insensitive)
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
key: Environment variable name
|
|
158
|
+
default: Default value if not set
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Parsed boolean value
|
|
162
|
+
"""
|
|
163
|
+
value = os.environ.get(key)
|
|
164
|
+
if value is None:
|
|
165
|
+
return default
|
|
166
|
+
|
|
167
|
+
value_lower = value.lower().strip()
|
|
168
|
+
if value_lower in ("true", "1", "yes", "on"):
|
|
169
|
+
return True
|
|
170
|
+
elif value_lower in ("false", "0", "no", "off", ""):
|
|
171
|
+
return False
|
|
172
|
+
else:
|
|
173
|
+
return default
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _get_timeout_from_env() -> int:
|
|
177
|
+
"""
|
|
178
|
+
Get execution timeout from environment variables.
|
|
179
|
+
|
|
180
|
+
Checks SKILLBOX_TIMEOUT_SECS first, then falls back to legacy EXECUTION_TIMEOUT.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Timeout in seconds
|
|
184
|
+
"""
|
|
185
|
+
# New environment variable (preferred)
|
|
186
|
+
value = os.environ.get("SKILLBOX_TIMEOUT_SECS")
|
|
187
|
+
if value:
|
|
188
|
+
try:
|
|
189
|
+
return int(value)
|
|
190
|
+
except ValueError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
# Legacy environment variable (deprecated)
|
|
194
|
+
value = os.environ.get("EXECUTION_TIMEOUT")
|
|
195
|
+
if value:
|
|
196
|
+
try:
|
|
197
|
+
return int(value)
|
|
198
|
+
except ValueError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
return DEFAULT_EXECUTION_TIMEOUT
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _get_memory_from_env() -> int:
|
|
205
|
+
"""
|
|
206
|
+
Get memory limit from environment variables.
|
|
207
|
+
|
|
208
|
+
Checks SKILLBOX_MAX_MEMORY_MB first, then falls back to legacy MAX_MEMORY_MB.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Memory limit in MB
|
|
212
|
+
"""
|
|
213
|
+
# New environment variable (preferred)
|
|
214
|
+
value = os.environ.get("SKILLBOX_MAX_MEMORY_MB")
|
|
215
|
+
if value:
|
|
216
|
+
try:
|
|
217
|
+
return int(value)
|
|
218
|
+
except ValueError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
# Legacy environment variable (deprecated)
|
|
222
|
+
value = os.environ.get("MAX_MEMORY_MB")
|
|
223
|
+
if value:
|
|
224
|
+
try:
|
|
225
|
+
return int(value)
|
|
226
|
+
except ValueError:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
return DEFAULT_MAX_MEMORY_MB
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skillbox sandbox implementation.
|
|
3
|
+
|
|
4
|
+
This module provides the Rust-based skillbox sandbox executor,
|
|
5
|
+
including binary management and execution logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .binary import (
|
|
9
|
+
BINARY_VERSION,
|
|
10
|
+
BINARY_NAME,
|
|
11
|
+
get_install_dir,
|
|
12
|
+
get_binary_path,
|
|
13
|
+
get_version_file,
|
|
14
|
+
get_platform,
|
|
15
|
+
get_download_url,
|
|
16
|
+
is_installed,
|
|
17
|
+
get_installed_version,
|
|
18
|
+
needs_update,
|
|
19
|
+
install,
|
|
20
|
+
uninstall,
|
|
21
|
+
find_binary,
|
|
22
|
+
ensure_installed,
|
|
23
|
+
)
|
|
24
|
+
from .executor import SkillboxExecutor
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Binary management
|
|
28
|
+
"BINARY_VERSION",
|
|
29
|
+
"BINARY_NAME",
|
|
30
|
+
"get_install_dir",
|
|
31
|
+
"get_binary_path",
|
|
32
|
+
"get_version_file",
|
|
33
|
+
"get_platform",
|
|
34
|
+
"get_download_url",
|
|
35
|
+
"is_installed",
|
|
36
|
+
"get_installed_version",
|
|
37
|
+
"needs_update",
|
|
38
|
+
"install",
|
|
39
|
+
"uninstall",
|
|
40
|
+
"find_binary",
|
|
41
|
+
"ensure_installed",
|
|
42
|
+
# Executor
|
|
43
|
+
"SkillboxExecutor",
|
|
44
|
+
]
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Binary management for the skillbox sandbox executor.
|
|
3
|
+
|
|
4
|
+
This module handles downloading, installing, and managing the Rust-based
|
|
5
|
+
sandbox binary, similar to how Playwright manages browser binaries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import shutil
|
|
12
|
+
import stat
|
|
13
|
+
import sys
|
|
14
|
+
import tarfile
|
|
15
|
+
import tempfile
|
|
16
|
+
import urllib.request
|
|
17
|
+
import zipfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, Tuple
|
|
20
|
+
|
|
21
|
+
# Version of the binary to download
|
|
22
|
+
BINARY_VERSION = "0.1.0"
|
|
23
|
+
|
|
24
|
+
# GitHub repository for releases
|
|
25
|
+
GITHUB_OWNER = "EXboys"
|
|
26
|
+
GITHUB_REPO = "skilllite"
|
|
27
|
+
|
|
28
|
+
# Base URL for downloading binaries
|
|
29
|
+
DOWNLOAD_BASE_URL = f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/download"
|
|
30
|
+
|
|
31
|
+
# Supported platforms: (system, machine) -> platform_name
|
|
32
|
+
PLATFORM_MAP = {
|
|
33
|
+
("Darwin", "arm64"): "darwin-arm64",
|
|
34
|
+
("Darwin", "x86_64"): "darwin-x64",
|
|
35
|
+
("Linux", "x86_64"): "linux-x64",
|
|
36
|
+
("Linux", "aarch64"): "linux-arm64",
|
|
37
|
+
("Linux", "arm64"): "linux-arm64",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Binary name per platform
|
|
41
|
+
BINARY_NAME = "skillbox"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_install_dir() -> Path:
|
|
45
|
+
"""
|
|
46
|
+
Get the installation directory for the skillbox binary.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Path to ~/.skillbox/bin/
|
|
50
|
+
"""
|
|
51
|
+
return Path.home() / ".skillbox" / "bin"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_binary_path() -> Path:
|
|
55
|
+
"""
|
|
56
|
+
Get the full path to the installed binary.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Path to ~/.skillbox/bin/skillbox
|
|
60
|
+
"""
|
|
61
|
+
return get_install_dir() / BINARY_NAME
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_version_file() -> Path:
|
|
65
|
+
"""
|
|
66
|
+
Get the path to the version file.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Path to ~/.skillbox/.version
|
|
70
|
+
"""
|
|
71
|
+
return Path.home() / ".skillbox" / ".version"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_platform() -> str:
|
|
75
|
+
"""
|
|
76
|
+
Detect the current platform.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Platform string like 'darwin-arm64', 'linux-x64', etc.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
RuntimeError: If the platform is not supported.
|
|
83
|
+
"""
|
|
84
|
+
system = platform.system()
|
|
85
|
+
machine = platform.machine()
|
|
86
|
+
|
|
87
|
+
key = (system, machine)
|
|
88
|
+
if key not in PLATFORM_MAP:
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"Unsupported platform: {system} {machine}. "
|
|
91
|
+
f"Supported platforms: macOS (x64, arm64), Linux (x64, arm64)"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return PLATFORM_MAP[key]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_download_url(version: Optional[str] = None) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Get the download URL for the current platform.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
version: Version to download. Defaults to BINARY_VERSION.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Full download URL for the binary.
|
|
106
|
+
"""
|
|
107
|
+
version = version or BINARY_VERSION
|
|
108
|
+
plat = get_platform()
|
|
109
|
+
|
|
110
|
+
# Binary naming convention: skillbox-{platform}.tar.gz
|
|
111
|
+
filename = f"skillbox-{plat}.tar.gz"
|
|
112
|
+
|
|
113
|
+
return f"{DOWNLOAD_BASE_URL}/v{version}/{filename}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_installed() -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Check if the skillbox binary is installed.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if the binary exists and is executable.
|
|
122
|
+
"""
|
|
123
|
+
binary_path = get_binary_path()
|
|
124
|
+
return binary_path.exists() and os.access(binary_path, os.X_OK)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_installed_version() -> Optional[str]:
|
|
128
|
+
"""
|
|
129
|
+
Get the version of the installed binary.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Version string, or None if not installed or version unknown.
|
|
133
|
+
"""
|
|
134
|
+
version_file = get_version_file()
|
|
135
|
+
if version_file.exists():
|
|
136
|
+
return version_file.read_text().strip()
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def needs_update(target_version: Optional[str] = None) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Check if the binary needs to be updated.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
target_version: Target version to check against.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if update is needed.
|
|
149
|
+
"""
|
|
150
|
+
if not is_installed():
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
target_version = target_version or BINARY_VERSION
|
|
154
|
+
installed_version = get_installed_version()
|
|
155
|
+
|
|
156
|
+
if installed_version is None:
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
return installed_version != target_version
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def download_with_progress(url: str, dest: Path, show_progress: bool = True) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Download a file with optional progress display.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
url: URL to download from.
|
|
168
|
+
dest: Destination path.
|
|
169
|
+
show_progress: Whether to show progress bar.
|
|
170
|
+
"""
|
|
171
|
+
def report_progress(block_num: int, block_size: int, total_size: int) -> None:
|
|
172
|
+
if not show_progress or total_size <= 0:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
downloaded = block_num * block_size
|
|
176
|
+
percent = min(100, downloaded * 100 // total_size)
|
|
177
|
+
bar_length = 40
|
|
178
|
+
filled = int(bar_length * percent // 100)
|
|
179
|
+
bar = "█" * filled + "░" * (bar_length - filled)
|
|
180
|
+
|
|
181
|
+
sys.stdout.write(f"\r Downloading: [{bar}] {percent}%")
|
|
182
|
+
sys.stdout.flush()
|
|
183
|
+
|
|
184
|
+
if downloaded >= total_size:
|
|
185
|
+
sys.stdout.write("\n")
|
|
186
|
+
sys.stdout.flush()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
urllib.request.urlretrieve(url, dest, reporthook=report_progress if show_progress else None)
|
|
190
|
+
except urllib.error.HTTPError as e:
|
|
191
|
+
if e.code == 404:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
f"Binary not found at {url}. "
|
|
194
|
+
f"Please check if version {BINARY_VERSION} has been released."
|
|
195
|
+
) from e
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def extract_archive(archive_path: Path, dest_dir: Path) -> Path:
|
|
200
|
+
"""
|
|
201
|
+
Extract a tar.gz or zip archive.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
archive_path: Path to the archive.
|
|
205
|
+
dest_dir: Directory to extract to.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Path to the extracted binary.
|
|
209
|
+
"""
|
|
210
|
+
if archive_path.suffix == ".gz" or str(archive_path).endswith(".tar.gz"):
|
|
211
|
+
with tarfile.open(archive_path, "r:gz") as tar:
|
|
212
|
+
tar.extractall(dest_dir)
|
|
213
|
+
elif archive_path.suffix == ".zip":
|
|
214
|
+
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
|
215
|
+
zip_ref.extractall(dest_dir)
|
|
216
|
+
else:
|
|
217
|
+
raise RuntimeError(f"Unknown archive format: {archive_path}")
|
|
218
|
+
|
|
219
|
+
# Find the binary in extracted files
|
|
220
|
+
binary_path = dest_dir / BINARY_NAME
|
|
221
|
+
if binary_path.exists():
|
|
222
|
+
return binary_path
|
|
223
|
+
|
|
224
|
+
# Check if it's in a subdirectory
|
|
225
|
+
for item in dest_dir.iterdir():
|
|
226
|
+
if item.is_dir():
|
|
227
|
+
nested_binary = item / BINARY_NAME
|
|
228
|
+
if nested_binary.exists():
|
|
229
|
+
return nested_binary
|
|
230
|
+
|
|
231
|
+
raise RuntimeError(f"Binary not found in archive: {archive_path}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def install(
|
|
235
|
+
version: Optional[str] = None,
|
|
236
|
+
force: bool = False,
|
|
237
|
+
show_progress: bool = True
|
|
238
|
+
) -> Path:
|
|
239
|
+
"""
|
|
240
|
+
Download and install the skillbox binary.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
version: Version to install. Defaults to BINARY_VERSION.
|
|
244
|
+
force: Force reinstall even if already installed.
|
|
245
|
+
show_progress: Show download progress.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Path to the installed binary.
|
|
249
|
+
"""
|
|
250
|
+
version = version or BINARY_VERSION
|
|
251
|
+
|
|
252
|
+
if not force and is_installed() and not needs_update(version):
|
|
253
|
+
installed_version = get_installed_version()
|
|
254
|
+
print(f"✓ skillbox v{installed_version} is already installed")
|
|
255
|
+
return get_binary_path()
|
|
256
|
+
|
|
257
|
+
plat = get_platform()
|
|
258
|
+
print(f"Installing skillbox v{version} for {plat}...")
|
|
259
|
+
|
|
260
|
+
# Create install directory
|
|
261
|
+
install_dir = get_install_dir()
|
|
262
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
|
|
264
|
+
# Download to temp directory
|
|
265
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
266
|
+
temp_path = Path(temp_dir)
|
|
267
|
+
archive_name = f"skillbox-{plat}.tar.gz"
|
|
268
|
+
archive_path = temp_path / archive_name
|
|
269
|
+
|
|
270
|
+
# Download
|
|
271
|
+
url = get_download_url(version)
|
|
272
|
+
print(f" Downloading from: {url}")
|
|
273
|
+
download_with_progress(url, archive_path, show_progress)
|
|
274
|
+
|
|
275
|
+
# Extract
|
|
276
|
+
print(" Extracting...")
|
|
277
|
+
extracted_binary = extract_archive(archive_path, temp_path)
|
|
278
|
+
|
|
279
|
+
# Move to install location
|
|
280
|
+
dest_binary = get_binary_path()
|
|
281
|
+
if dest_binary.exists():
|
|
282
|
+
dest_binary.unlink()
|
|
283
|
+
|
|
284
|
+
shutil.move(str(extracted_binary), str(dest_binary))
|
|
285
|
+
|
|
286
|
+
# Make executable
|
|
287
|
+
dest_binary.chmod(dest_binary.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
288
|
+
|
|
289
|
+
# Write version file
|
|
290
|
+
version_file = get_version_file()
|
|
291
|
+
version_file.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
version_file.write_text(version)
|
|
293
|
+
|
|
294
|
+
print(f"✓ Successfully installed skillbox v{version}")
|
|
295
|
+
print(f" Location: {dest_binary}")
|
|
296
|
+
|
|
297
|
+
return dest_binary
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def uninstall() -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Uninstall the skillbox binary.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
True if uninstalled, False if not installed.
|
|
306
|
+
"""
|
|
307
|
+
binary_path = get_binary_path()
|
|
308
|
+
version_file = get_version_file()
|
|
309
|
+
|
|
310
|
+
if not binary_path.exists():
|
|
311
|
+
print("skillbox is not installed")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
binary_path.unlink()
|
|
315
|
+
if version_file.exists():
|
|
316
|
+
version_file.unlink()
|
|
317
|
+
|
|
318
|
+
print("✓ Successfully uninstalled skillbox")
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def find_binary() -> Optional[str]:
|
|
323
|
+
"""
|
|
324
|
+
Find the skillbox binary.
|
|
325
|
+
|
|
326
|
+
Search order:
|
|
327
|
+
1. ~/.skillbox/bin/skillbox (installed by this package)
|
|
328
|
+
2. System PATH
|
|
329
|
+
3. ~/.cargo/bin/skillbox (cargo install)
|
|
330
|
+
4. Common system locations
|
|
331
|
+
5. Development build locations
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Path to the binary, or None if not found.
|
|
335
|
+
"""
|
|
336
|
+
# 1. Check our install location first
|
|
337
|
+
our_binary = get_binary_path()
|
|
338
|
+
if our_binary.exists() and os.access(our_binary, os.X_OK):
|
|
339
|
+
return str(our_binary)
|
|
340
|
+
|
|
341
|
+
# 2. Check PATH
|
|
342
|
+
path_binary = shutil.which(BINARY_NAME)
|
|
343
|
+
if path_binary:
|
|
344
|
+
return path_binary
|
|
345
|
+
|
|
346
|
+
# 3. Check cargo install location
|
|
347
|
+
cargo_binary = Path.home() / ".cargo" / "bin" / BINARY_NAME
|
|
348
|
+
if cargo_binary.exists():
|
|
349
|
+
return str(cargo_binary)
|
|
350
|
+
|
|
351
|
+
# 4. Check common system locations
|
|
352
|
+
system_locations = [
|
|
353
|
+
Path("/usr/local/bin") / BINARY_NAME,
|
|
354
|
+
Path("/usr/bin") / BINARY_NAME,
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
for loc in system_locations:
|
|
358
|
+
if loc.exists() and os.access(loc, os.X_OK):
|
|
359
|
+
return str(loc)
|
|
360
|
+
|
|
361
|
+
# 5. Check development build locations (relative to common project structures)
|
|
362
|
+
dev_locations = [
|
|
363
|
+
Path("skillbox/target/release") / BINARY_NAME,
|
|
364
|
+
Path("../skillbox/target/release") / BINARY_NAME,
|
|
365
|
+
Path("../../skillbox/target/release") / BINARY_NAME,
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
for loc in dev_locations:
|
|
369
|
+
if loc.exists() and os.access(loc, os.X_OK):
|
|
370
|
+
return str(loc.resolve())
|
|
371
|
+
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def ensure_installed(
|
|
376
|
+
auto_install: bool = True,
|
|
377
|
+
show_progress: bool = True
|
|
378
|
+
) -> str:
|
|
379
|
+
"""
|
|
380
|
+
Ensure the skillbox binary is installed and return its path.
|
|
381
|
+
|
|
382
|
+
This is the main entry point for getting a working binary path.
|
|
383
|
+
It will:
|
|
384
|
+
1. Try to find an existing binary
|
|
385
|
+
2. If not found and auto_install is True, download and install it
|
|
386
|
+
3. Raise an error if binary cannot be found or installed
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
auto_install: Automatically install if not found.
|
|
390
|
+
show_progress: Show download progress during installation.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Path to the binary.
|
|
394
|
+
|
|
395
|
+
Raises:
|
|
396
|
+
FileNotFoundError: If binary not found and auto_install is False.
|
|
397
|
+
RuntimeError: If installation fails.
|
|
398
|
+
"""
|
|
399
|
+
# First, try to find existing binary
|
|
400
|
+
existing = find_binary()
|
|
401
|
+
if existing:
|
|
402
|
+
return existing
|
|
403
|
+
|
|
404
|
+
# Not found - try to install if allowed
|
|
405
|
+
if auto_install:
|
|
406
|
+
try:
|
|
407
|
+
installed_path = install(show_progress=show_progress)
|
|
408
|
+
return str(installed_path)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
raise RuntimeError(
|
|
411
|
+
f"Failed to install skillbox binary: {e}\n"
|
|
412
|
+
f"You can manually install it with: skilllite install"
|
|
413
|
+
) from e
|
|
414
|
+
|
|
415
|
+
# Not found and not allowed to install
|
|
416
|
+
raise FileNotFoundError(
|
|
417
|
+
"skillbox binary not found. Install it with:\n"
|
|
418
|
+
" skilllite install\n"
|
|
419
|
+
"Or build from source:\n"
|
|
420
|
+
" cd skillbox && cargo build --release"
|
|
421
|
+
)
|