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.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. 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}")
@@ -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")