illusion-code 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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ripgrep (rg) 二进制的发现、下载、缓存和执行模块。
|
|
3
|
+
|
|
4
|
+
核心原则:让 rg 去碰文件系统,Python 只处理结果。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RipgrepNotFoundError(Exception):
|
|
15
|
+
"""rg 不可用时抛出"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RipgrepError(Exception):
|
|
20
|
+
"""rg 执行失败时抛出"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
# 平台映射表:平台键 -> (rg 目标三元组, 归档格式)
|
|
24
|
+
PLATFORM_MAP = {
|
|
25
|
+
"x64-win32": ("x86_64-pc-windows-msvc", "zip"),
|
|
26
|
+
"arm64-win32": ("aarch64-pc-windows-msvc", "zip"),
|
|
27
|
+
"x64-darwin": ("x86_64-apple-darwin", "tar.gz"),
|
|
28
|
+
"arm64-darwin": ("aarch64-apple-darwin", "tar.gz"),
|
|
29
|
+
"x64-linux": ("x86_64-unknown-linux-musl", "tar.gz"),
|
|
30
|
+
"arm64-linux": ("aarch64-unknown-linux-gnu", "tar.gz"),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_platform_key() -> str:
|
|
35
|
+
"""
|
|
36
|
+
获取当前平台的键名。
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
平台键,如 "x64-win32"、"arm64-darwin"
|
|
40
|
+
"""
|
|
41
|
+
machine = platform.machine().lower()
|
|
42
|
+
plat = sys.platform
|
|
43
|
+
|
|
44
|
+
# 标准化架构名称
|
|
45
|
+
if machine in ("x86_64", "amd64"):
|
|
46
|
+
arch = "x64"
|
|
47
|
+
elif machine in ("aarch64", "arm64"):
|
|
48
|
+
arch = "arm64"
|
|
49
|
+
else:
|
|
50
|
+
arch = machine
|
|
51
|
+
|
|
52
|
+
return f"{arch}-{plat}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_rg_binary_name(platform_key: str) -> str:
|
|
56
|
+
"""
|
|
57
|
+
获取 rg 二进制文件名。
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
platform_key: 平台键
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
二进制文件名,Windows 上为 "rg.exe",其他为 "rg"
|
|
64
|
+
"""
|
|
65
|
+
if platform_key.endswith("-win32"):
|
|
66
|
+
return "rg.exe"
|
|
67
|
+
return "rg"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_cache_dir() -> str:
|
|
71
|
+
"""
|
|
72
|
+
获取 rg 缓存目录路径。
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
缓存目录路径:~/.illusion/ripgrep/
|
|
76
|
+
"""
|
|
77
|
+
return os.path.join(str(Path.home()), ".illusion", "ripgrep")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def find_rg_path() -> str:
|
|
81
|
+
"""
|
|
82
|
+
查找 rg 二进制路径。
|
|
83
|
+
|
|
84
|
+
优先级:
|
|
85
|
+
1. 环境变量 ILLUSION_RIPGREP_PATH
|
|
86
|
+
2. 本地缓存 ~/.illusion/ripgrep/rg
|
|
87
|
+
3. 系统 PATH
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
rg 二进制路径
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
RipgrepNotFoundError: rg 不可用时
|
|
94
|
+
"""
|
|
95
|
+
# 1. 检查环境变量
|
|
96
|
+
env_path = os.environ.get("ILLUSION_RIPGREP_PATH")
|
|
97
|
+
if env_path and os.path.exists(env_path):
|
|
98
|
+
return env_path
|
|
99
|
+
|
|
100
|
+
# 2. 检查本地缓存
|
|
101
|
+
cache_dir = get_cache_dir()
|
|
102
|
+
platform_key = get_platform_key()
|
|
103
|
+
binary_name = get_rg_binary_name(platform_key)
|
|
104
|
+
cache_path = os.path.join(cache_dir, binary_name)
|
|
105
|
+
if os.path.exists(cache_path):
|
|
106
|
+
return cache_path
|
|
107
|
+
|
|
108
|
+
# 3. 检查系统 PATH
|
|
109
|
+
system_path = shutil.which("rg")
|
|
110
|
+
if system_path:
|
|
111
|
+
return system_path
|
|
112
|
+
|
|
113
|
+
# 4. 以上均不可用
|
|
114
|
+
raise RipgrepNotFoundError(
|
|
115
|
+
"ripgrep (rg) 不可用。请手动安装 rg 或设置环境变量 ILLUSION_RIPGREP_PATH。"
|
|
116
|
+
"下载地址:https://github.com/BurntSushi/ripgrep/releases"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# GitHub Releases 下载地址模板
|
|
121
|
+
RG_DOWNLOAD_URL = "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/{archive}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def extract_zip(zip_path: str, extract_dir: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
解压 ZIP 文件到指定目录。
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
zip_path: ZIP 文件路径
|
|
130
|
+
extract_dir: 解压目标目录
|
|
131
|
+
"""
|
|
132
|
+
import zipfile
|
|
133
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
134
|
+
# 查找 rg.exe 文件
|
|
135
|
+
for name in zf.namelist():
|
|
136
|
+
if name.endswith("rg.exe"):
|
|
137
|
+
# 提取到目标目录
|
|
138
|
+
zf.extract(name, extract_dir)
|
|
139
|
+
# 移动到目标目录根目录
|
|
140
|
+
extracted = os.path.join(extract_dir, name)
|
|
141
|
+
target = os.path.join(extract_dir, "rg.exe")
|
|
142
|
+
if extracted != target:
|
|
143
|
+
os.rename(extracted, target)
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def extract_tar(tar_path: str, extract_dir: str) -> None:
|
|
148
|
+
"""
|
|
149
|
+
解压 TAR.GZ 文件到指定目录。
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
tar_path: TAR.GZ 文件路径
|
|
153
|
+
extract_dir: 解压目标目录
|
|
154
|
+
"""
|
|
155
|
+
import tarfile
|
|
156
|
+
with tarfile.open(tar_path, "r:gz") as tf:
|
|
157
|
+
# 查找 rg 文件
|
|
158
|
+
for member in tf.getmembers():
|
|
159
|
+
if member.name.endswith("/rg") or member.name == "rg":
|
|
160
|
+
# 提取到目标目录
|
|
161
|
+
tf.extract(member, extract_dir)
|
|
162
|
+
# 移动到目标目录根目录
|
|
163
|
+
extracted = os.path.join(extract_dir, member.name)
|
|
164
|
+
target = os.path.join(extract_dir, "rg")
|
|
165
|
+
if extracted != target:
|
|
166
|
+
os.rename(extracted, target)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def download_rg() -> str:
|
|
171
|
+
"""
|
|
172
|
+
下载 rg 二进制到缓存目录。
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
下载后的 rg 二进制路径
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
RipgrepNotFoundError: 下载失败时
|
|
179
|
+
"""
|
|
180
|
+
import tempfile
|
|
181
|
+
import urllib.request
|
|
182
|
+
|
|
183
|
+
platform_key = get_platform_key()
|
|
184
|
+
if platform_key not in PLATFORM_MAP:
|
|
185
|
+
raise RipgrepNotFoundError(f"不支持的平台: {platform_key}")
|
|
186
|
+
|
|
187
|
+
target_triple, archive_format = PLATFORM_MAP[platform_key]
|
|
188
|
+
binary_name = get_rg_binary_name(platform_key)
|
|
189
|
+
cache_dir = get_cache_dir()
|
|
190
|
+
|
|
191
|
+
# 创建缓存目录
|
|
192
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
193
|
+
|
|
194
|
+
# 构建下载 URL
|
|
195
|
+
if archive_format == "zip":
|
|
196
|
+
archive_name = f"ripgrep-14.1.1-{target_triple}.zip"
|
|
197
|
+
else:
|
|
198
|
+
archive_name = f"ripgrep-14.1.1-{target_triple}.tar.gz"
|
|
199
|
+
url = RG_DOWNLOAD_URL.format(archive=archive_name)
|
|
200
|
+
|
|
201
|
+
# 下载文件
|
|
202
|
+
try:
|
|
203
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{archive_format}") as tmp:
|
|
204
|
+
tmp_path = tmp.name
|
|
205
|
+
urllib.request.urlretrieve(url, tmp_path)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise RipgrepNotFoundError(f"下载 rg 失败: {e}")
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# 解压文件
|
|
211
|
+
if archive_format == "zip":
|
|
212
|
+
extract_zip(tmp_path, cache_dir)
|
|
213
|
+
else:
|
|
214
|
+
extract_tar(tmp_path, cache_dir)
|
|
215
|
+
|
|
216
|
+
# 设置权限(非 Windows)
|
|
217
|
+
rg_path = os.path.join(cache_dir, binary_name)
|
|
218
|
+
if sys.platform != "win32":
|
|
219
|
+
os.chmod(rg_path, 0o755)
|
|
220
|
+
|
|
221
|
+
return rg_path
|
|
222
|
+
finally:
|
|
223
|
+
# 清理临时文件
|
|
224
|
+
if os.path.exists(tmp_path):
|
|
225
|
+
os.unlink(tmp_path)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def ensure_ripgrep() -> str:
|
|
229
|
+
"""
|
|
230
|
+
确保 rg 可用,返回二进制路径。
|
|
231
|
+
|
|
232
|
+
优先级:env > cache > PATH > download
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
rg 二进制路径
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
RipgrepNotFoundError: rg 不可用时
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
return find_rg_path()
|
|
242
|
+
except RipgrepNotFoundError:
|
|
243
|
+
# 尝试下载
|
|
244
|
+
return download_rg()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def run_rg(args: list[str], cwd: str | None = None,
|
|
248
|
+
timeout: float = 20.0) -> tuple[str, str, int]:
|
|
249
|
+
"""
|
|
250
|
+
执行 rg 命令。
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
args: rg 命令行参数
|
|
254
|
+
cwd: 工作目录
|
|
255
|
+
timeout: 超时时间(秒)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
(stdout, stderr, returncode) 元组
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
RipgrepError: 执行失败或超时时
|
|
262
|
+
"""
|
|
263
|
+
import asyncio
|
|
264
|
+
import subprocess
|
|
265
|
+
|
|
266
|
+
rg_path = await ensure_ripgrep()
|
|
267
|
+
|
|
268
|
+
# 构建完整命令
|
|
269
|
+
cmd = [rg_path] + args
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# 创建子进程
|
|
273
|
+
process = await asyncio.create_subprocess_exec(
|
|
274
|
+
*cmd,
|
|
275
|
+
stdout=asyncio.subprocess.PIPE,
|
|
276
|
+
stderr=asyncio.subprocess.PIPE,
|
|
277
|
+
cwd=cwd,
|
|
278
|
+
# Windows 特殊处理
|
|
279
|
+
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# 等待完成,带超时
|
|
283
|
+
try:
|
|
284
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
285
|
+
process.communicate(), timeout=timeout
|
|
286
|
+
)
|
|
287
|
+
except asyncio.TimeoutError:
|
|
288
|
+
process.kill()
|
|
289
|
+
await process.wait()
|
|
290
|
+
raise RipgrepError(f"rg 执行超时({timeout}秒)")
|
|
291
|
+
|
|
292
|
+
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
|
293
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
294
|
+
return stdout, stderr, process.returncode
|
|
295
|
+
|
|
296
|
+
except RipgrepError:
|
|
297
|
+
raise
|
|
298
|
+
except Exception as e:
|
|
299
|
+
raise RipgrepError(f"rg 执行失败: {e}")
|
illusion/utils/shell.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell 和子进程辅助函数模块
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
本模块提供 shell 命令执行和子进程创建的跨平台支持功能。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 解析适合当前平台的最佳 shell 命令
|
|
9
|
+
- 创建带有沙箱支持的异步子进程
|
|
10
|
+
- 在 Windows 上智能查找可用的 bash 可执行文件
|
|
11
|
+
|
|
12
|
+
函数说明:
|
|
13
|
+
- resolve_shell_command: 返回当前平台的最佳 shell 命令 argv
|
|
14
|
+
- create_shell_subprocess: 创建带有沙箱支持的 shell 子进程
|
|
15
|
+
- _resolve_windows_bash: 解析 Windows 上可用的 bash 可执行文件
|
|
16
|
+
|
|
17
|
+
使用示例:
|
|
18
|
+
>>> from illusion.utils import resolve_shell_command
|
|
19
|
+
|
|
20
|
+
>>> # 获取当前平台的 shell 命令
|
|
21
|
+
>>> argv = resolve_shell_command("echo hello")
|
|
22
|
+
>>> print(argv) # ['bash', '-lc', 'echo hello']
|
|
23
|
+
|
|
24
|
+
>>> # 创建子进程
|
|
25
|
+
>>> process = await create_shell_subprocess("ls", cwd="/tmp")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import asyncio
|
|
31
|
+
import os
|
|
32
|
+
import shutil
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from illusion.config import Settings, load_settings
|
|
39
|
+
from illusion.platforms import PlatformName, get_platform
|
|
40
|
+
from illusion.sandbox import wrap_command_for_sandbox
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_shell_command(
|
|
44
|
+
command: str,
|
|
45
|
+
*,
|
|
46
|
+
platform_name: PlatformName | None = None,
|
|
47
|
+
) -> list[str]:
|
|
48
|
+
"""
|
|
49
|
+
解析适合当前平台的最佳 shell 命令
|
|
50
|
+
|
|
51
|
+
根据平台类型自动选择最优的 shell 解释器:
|
|
52
|
+
- Windows: 优先 WSL bash,其次 PowerShell,最后 cmd.exe
|
|
53
|
+
- Unix/Linux/macOS: 优先 bash,其次 sh
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
command: 要执行的 shell 命令字符串
|
|
57
|
+
platform_name: 指定平台名称,默认自动检测
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
list[str]: shell 命令的 argv 列表,第一个元素为可执行文件路径
|
|
61
|
+
|
|
62
|
+
使用示例:
|
|
63
|
+
>>> argv = resolve_shell_command("ls -la")
|
|
64
|
+
>>> argv # ['bash', '-lc', 'ls -la']
|
|
65
|
+
"""
|
|
66
|
+
resolved_platform = platform_name or get_platform()
|
|
67
|
+
# Windows 平台优先尝试 WSL bash
|
|
68
|
+
if resolved_platform == "windows":
|
|
69
|
+
bash = _resolve_windows_bash()
|
|
70
|
+
if bash:
|
|
71
|
+
return [bash, "-lc", command]
|
|
72
|
+
powershell = shutil.which("pwsh") or shutil.which("powershell")
|
|
73
|
+
if powershell:
|
|
74
|
+
return [powershell, "-NoLogo", "-NoProfile", "-Command", command]
|
|
75
|
+
return [shutil.which("cmd.exe") or "cmd.exe", "/d", "/s", "/c", command]
|
|
76
|
+
|
|
77
|
+
# Unix 系统优先使用 bash
|
|
78
|
+
bash = shutil.which("bash")
|
|
79
|
+
if bash:
|
|
80
|
+
return [bash, "-lc", command]
|
|
81
|
+
shell = shutil.which("sh") or os.environ.get("SHELL") or "/bin/sh"
|
|
82
|
+
return [shell, "-lc", command]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def create_shell_subprocess(
|
|
86
|
+
command: str,
|
|
87
|
+
*,
|
|
88
|
+
cwd: str | Path,
|
|
89
|
+
settings: Settings | None = None,
|
|
90
|
+
disable_sandbox: bool = False,
|
|
91
|
+
stdin: int | None = None,
|
|
92
|
+
stdout: int | None = None,
|
|
93
|
+
stderr: int | None = None,
|
|
94
|
+
env: Mapping[str, str] | None = None,
|
|
95
|
+
) -> asyncio.subprocess.Process:
|
|
96
|
+
"""
|
|
97
|
+
创建带有平台感知和沙箱支持的 shell 子进程
|
|
98
|
+
|
|
99
|
+
自动解析适合平台的 shell 命令,并应用沙箱包装(如果启用)。
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
command: 要执行的 shell 命令
|
|
103
|
+
cwd: 工作目录
|
|
104
|
+
settings: 配置对象,默认自动加载
|
|
105
|
+
disable_sandbox: 是否绕过沙箱包装
|
|
106
|
+
stdin: 标准输入文件描述符
|
|
107
|
+
stdout: 标准输出文件描述符
|
|
108
|
+
stderr: 标准错误文件描述符
|
|
109
|
+
environment: 环境变量映射
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
asyncio.subprocess.Process: 异步子进程对象
|
|
113
|
+
|
|
114
|
+
使用示例:
|
|
115
|
+
>>> process = await create_shell_subprocess("ls", cwd="/tmp")
|
|
116
|
+
>>> await process.wait()
|
|
117
|
+
"""
|
|
118
|
+
resolved_settings = settings or load_settings()
|
|
119
|
+
argv = resolve_shell_command(command)
|
|
120
|
+
# 使用沙箱包装命令(如果配置启用且未显式禁用)
|
|
121
|
+
if disable_sandbox:
|
|
122
|
+
cleanup_path = None
|
|
123
|
+
else:
|
|
124
|
+
argv, cleanup_path = wrap_command_for_sandbox(argv, settings=resolved_settings)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
kwargs: dict = {}
|
|
128
|
+
if sys.platform == "win32":
|
|
129
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
130
|
+
process = await asyncio.create_subprocess_exec(
|
|
131
|
+
*argv,
|
|
132
|
+
cwd=str(Path(cwd).resolve()),
|
|
133
|
+
stdin=stdin,
|
|
134
|
+
stdout=stdout,
|
|
135
|
+
stderr=stderr,
|
|
136
|
+
env=dict(env) if env is not None else None,
|
|
137
|
+
**kwargs,
|
|
138
|
+
)
|
|
139
|
+
except Exception:
|
|
140
|
+
# 发生异常时清理沙箱临时文件
|
|
141
|
+
if cleanup_path is not None:
|
|
142
|
+
cleanup_path.unlink(missing_ok=True)
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
# 进程结束后异步清理沙箱临时文件
|
|
146
|
+
if cleanup_path is not None:
|
|
147
|
+
asyncio.create_task(_cleanup_after_exit(process, cleanup_path))
|
|
148
|
+
return process
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def _cleanup_after_exit(process: asyncio.subprocess.Process, cleanup_path: Path) -> None:
|
|
152
|
+
"""
|
|
153
|
+
进程退出后清理沙箱临时文件
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
process: 要监控的子进程
|
|
157
|
+
cleanup_path: 需要清理的文件路径
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
await process.wait()
|
|
161
|
+
finally:
|
|
162
|
+
cleanup_path.unlink(missing_ok=True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _resolve_windows_bash() -> str | None:
|
|
166
|
+
"""
|
|
167
|
+
解析 Windows 上可用的 bash 可执行文件
|
|
168
|
+
|
|
169
|
+
忽略传统的 Windows 系统 shim(C:\\Windows\\System32\\bash.exe),
|
|
170
|
+
该位置可能在未配置 WSL 的机器上失败或输出无法读取的内容。
|
|
171
|
+
|
|
172
|
+
解析优先级:
|
|
173
|
+
1. ILLUSION_CODE_GIT_BASH_PATH 环境变量覆盖
|
|
174
|
+
2. 通过 PATH 找到的 bash(排除 system32 shim)
|
|
175
|
+
3. 从 git 可执行文件位置解析 bash
|
|
176
|
+
4. 在已知的 Git for Windows 安装路径中查找
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
str | None: bash 可执行文件路径,未找到则返回 None
|
|
180
|
+
"""
|
|
181
|
+
# 1. 通过环境变量显式指定
|
|
182
|
+
env_bash = os.environ.get("ILLUSION_CODE_GIT_BASH_PATH")
|
|
183
|
+
if env_bash and Path(env_bash).exists():
|
|
184
|
+
return env_bash
|
|
185
|
+
|
|
186
|
+
# 2. PATH 上的 bash(跳过传统的 system32 shim)
|
|
187
|
+
bash = shutil.which("bash")
|
|
188
|
+
if bash and not _is_windows_bash_shim(bash):
|
|
189
|
+
return bash
|
|
190
|
+
|
|
191
|
+
# 3. 从 git 可执行文件位置解析 bash
|
|
192
|
+
git_path = shutil.which("git")
|
|
193
|
+
if git_path:
|
|
194
|
+
# git.exe 通常位于 <Git-Root>\cmd\git.exe 或 <Git-Root>\bin\git.exe
|
|
195
|
+
# bash.exe 位于 <Git-Root>\bin\bash.exe
|
|
196
|
+
git_root = Path(git_path).resolve().parent.parent
|
|
197
|
+
bash_via_git = git_root / "bin" / "bash.exe"
|
|
198
|
+
if bash_via_git.exists():
|
|
199
|
+
return str(bash_via_git)
|
|
200
|
+
|
|
201
|
+
# 4. 在已知的 Git for Windows 安装路径中搜索
|
|
202
|
+
for candidate in _windows_git_bash_candidates():
|
|
203
|
+
if candidate.exists():
|
|
204
|
+
return str(candidate)
|
|
205
|
+
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _windows_git_bash_candidates() -> list[Path]:
|
|
210
|
+
"""
|
|
211
|
+
生成已知的 Git for Windows 安装路径候选列表
|
|
212
|
+
|
|
213
|
+
在常见的 Program Files 目录中查找 Git 安装路径。
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
list[Path]: 可能的 bash.exe 路径列表
|
|
217
|
+
"""
|
|
218
|
+
roots: list[str] = []
|
|
219
|
+
for key in ("ProgramFiles", "ProgramFiles(x86)", "LocalAppData"):
|
|
220
|
+
value = os.environ.get(key)
|
|
221
|
+
if value:
|
|
222
|
+
roots.append(value)
|
|
223
|
+
|
|
224
|
+
candidates: list[Path] = []
|
|
225
|
+
for root in roots:
|
|
226
|
+
base = Path(root)
|
|
227
|
+
candidates.append(base / "Git" / "bin" / "bash.exe")
|
|
228
|
+
candidates.append(base / "Git" / "usr" / "bin" / "bash.exe")
|
|
229
|
+
candidates.append(base / "Programs" / "Git" / "bin" / "bash.exe")
|
|
230
|
+
candidates.append(base / "Programs" / "Git" / "usr" / "bin" / "bash.exe")
|
|
231
|
+
return candidates
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _is_windows_bash_shim(path: str) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
检查路径是否为 Windows system32 bash shim
|
|
237
|
+
|
|
238
|
+
判断给定路径是否为传统的 Windows system32 bash 替身(shim),
|
|
239
|
+
这是一个空壳程序,不提供真正的 bash 功能。
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
path: 要检查的可执行文件路径
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
bool: 是否为 system32 shim
|
|
246
|
+
"""
|
|
247
|
+
normalized = path.replace("/", "\\").lower()
|
|
248
|
+
return normalized.endswith("\\windows\\system32\\bash.exe")
|