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
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Worktree 隔离模块
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
本模块提供 swarm 代理的 Git worktree 隔离功能。
|
|
6
|
+
使用 Git worktree 实现代理间的文件系统隔离。
|
|
7
|
+
|
|
8
|
+
主要组件:
|
|
9
|
+
- WorktreeManager: Git worktree 管理器
|
|
10
|
+
- WorktreeInfo: Worktree 元数据
|
|
11
|
+
- validate_worktree_slug: Worktree slug 验证函数
|
|
12
|
+
|
|
13
|
+
使用示例:
|
|
14
|
+
>>> from illusion.swarm.worktree import WorktreeManager
|
|
15
|
+
>>>
|
|
16
|
+
>>> manager = WorktreeManager()
|
|
17
|
+
>>> info = await manager.create_worktree(repo_path, "researcher-1", agent_id="agent-1")
|
|
18
|
+
>>> print(f"Worktree created at: {info.path}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Slug 验证
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
# 有效的路径段正则
|
|
37
|
+
_VALID_SEGMENT = re.compile(r"^[a-zA-Z0-9._-]+$")
|
|
38
|
+
# 最大 slug 长度
|
|
39
|
+
_MAX_SLUG_LENGTH = 64
|
|
40
|
+
# 常见符号链接目录
|
|
41
|
+
_COMMON_SYMLINK_DIRS = ("node_modules", ".venv", "__pycache__", ".tox")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_worktree_slug(slug: str) -> str:
|
|
45
|
+
"""清理并验证 worktree slug。
|
|
46
|
+
|
|
47
|
+
规则:
|
|
48
|
+
- 最多 64 个字符
|
|
49
|
+
- 每个 '/' 分隔的段必须匹配 [a-zA-Z0-9._-]+
|
|
50
|
+
- 拒绝 '.' 和 '..' 段(路径遍历)
|
|
51
|
+
- 拒绝前导/尾随 '/'
|
|
52
|
+
|
|
53
|
+
如果有效则返回 slug 不变,否则引发 ValueError。
|
|
54
|
+
"""
|
|
55
|
+
if not slug:
|
|
56
|
+
raise ValueError("Worktree slug must not be empty")
|
|
57
|
+
|
|
58
|
+
if len(slug) > _MAX_SLUG_LENGTH:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Worktree slug must be {_MAX_SLUG_LENGTH} characters or fewer (got {len(slug)})"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# 拒绝绝对路径
|
|
64
|
+
if slug.startswith("/") or slug.startswith("\\"):
|
|
65
|
+
raise ValueError(f"Worktree slug must not be an absolute path: {slug!r}")
|
|
66
|
+
|
|
67
|
+
for segment in slug.split("/"):
|
|
68
|
+
if segment in (".", ".."):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Worktree slug {slug!r}: must not contain '.' or '..' path segments"
|
|
71
|
+
)
|
|
72
|
+
if not _VALID_SEGMENT.match(segment):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Worktree slug {slug!r}: each segment must be non-empty and contain only "
|
|
75
|
+
"letters, digits, dots, underscores, and dashes"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return slug
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# 数据结构
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class WorktreeInfo:
|
|
87
|
+
"""管理的 git worktree 的元数据。"""
|
|
88
|
+
|
|
89
|
+
slug: str
|
|
90
|
+
path: Path
|
|
91
|
+
branch: str
|
|
92
|
+
original_path: Path
|
|
93
|
+
created_at: float
|
|
94
|
+
agent_id: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# 内部辅助函数
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _flatten_slug(slug: str) -> str:
|
|
102
|
+
"""用 '+' 替换 '/' 以避免嵌套目录/分支问题。"""
|
|
103
|
+
return slug.replace("/", "+")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _worktree_branch(slug: str) -> str:
|
|
107
|
+
"""生成 worktree 分支名称。"""
|
|
108
|
+
return f"worktree-{_flatten_slug(slug)}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _run_git(*args: str, cwd: Path) -> tuple[int, str, str]:
|
|
112
|
+
"""运行 git 命令,返回 (returncode, stdout, stderr)。"""
|
|
113
|
+
kwargs: dict = {}
|
|
114
|
+
if sys.platform == "win32":
|
|
115
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
116
|
+
proc = await asyncio.create_subprocess_exec(
|
|
117
|
+
"git",
|
|
118
|
+
*args,
|
|
119
|
+
cwd=str(cwd),
|
|
120
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
121
|
+
stdout=asyncio.subprocess.PIPE,
|
|
122
|
+
stderr=asyncio.subprocess.PIPE,
|
|
123
|
+
env={**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": ""},
|
|
124
|
+
**kwargs,
|
|
125
|
+
)
|
|
126
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
127
|
+
return (
|
|
128
|
+
proc.returncode or 0,
|
|
129
|
+
stdout_bytes.decode(errors="replace").strip(),
|
|
130
|
+
stderr_bytes.decode(errors="replace").strip(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def _symlink_common_dirs(repo_path: Path, worktree_path: Path) -> None:
|
|
135
|
+
"""从主仓库符号链接大型公共目录以避免重复。"""
|
|
136
|
+
for dir_name in _COMMON_SYMLINK_DIRS:
|
|
137
|
+
src = repo_path / dir_name
|
|
138
|
+
dst = worktree_path / dir_name
|
|
139
|
+
if dst.exists() or dst.is_symlink():
|
|
140
|
+
continue
|
|
141
|
+
if not src.exists():
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
dst.symlink_to(src)
|
|
145
|
+
except OSError:
|
|
146
|
+
pass # 非致命:磁盘满、不支持的文件系统等
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _remove_symlinks(worktree_path: Path) -> None:
|
|
150
|
+
"""移除由 _symlink_common_dirs 创建的符号链接。"""
|
|
151
|
+
for dir_name in _COMMON_SYMLINK_DIRS:
|
|
152
|
+
dst = worktree_path / dir_name
|
|
153
|
+
if dst.is_symlink():
|
|
154
|
+
try:
|
|
155
|
+
dst.unlink()
|
|
156
|
+
except OSError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# WorktreeManager
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
class WorktreeManager:
|
|
165
|
+
"""管理隔离代理执行的 git worktree。
|
|
166
|
+
|
|
167
|
+
Worktree 存储在 ``base_dir/<slug>/`` 下('/' 替换为 '+' 以保持布局扁平)。
|
|
168
|
+
JSON 元数据文件跟踪活跃 worktree 及其关联的代理 ID,以便可以清理过期的 worktree。
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, base_dir: Path | None = None) -> None:
|
|
172
|
+
"""初始化 WorktreeManager。"""
|
|
173
|
+
self.base_dir: Path = base_dir or Path.home() / ".illusion" / "worktrees"
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
# 公开 API
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
async def create_worktree(
|
|
180
|
+
self,
|
|
181
|
+
repo_path: Path,
|
|
182
|
+
slug: str,
|
|
183
|
+
branch: str | None = None,
|
|
184
|
+
agent_id: str | None = None,
|
|
185
|
+
) -> WorktreeInfo:
|
|
186
|
+
"""为 *slug* 创建(或恢复)git worktree。
|
|
187
|
+
|
|
188
|
+
如果 worktree 目录已存在且是有效的 git worktree,
|
|
189
|
+
则在不重新运行 ``git worktree add`` 的情况下恢复。
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
repo_path: 主仓库的绝对路径。
|
|
193
|
+
slug: 人类可读标识符(通过 validate_worktree_slug 验证)。
|
|
194
|
+
branch: 要检出的分支名称;默认为生成的 ``worktree-<slug>`` 名称。
|
|
195
|
+
agent_id: 拥有此 worktree 的代理的可选标识符。
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
描述 worktree 的 WorktreeInfo。
|
|
199
|
+
"""
|
|
200
|
+
# 验证 slug
|
|
201
|
+
validate_worktree_slug(slug)
|
|
202
|
+
repo_path = repo_path.resolve()
|
|
203
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
|
|
205
|
+
# 扁平化 slug 并构建路径
|
|
206
|
+
flat_slug = _flatten_slug(slug)
|
|
207
|
+
worktree_path = self.base_dir / flat_slug
|
|
208
|
+
worktree_branch = branch or _worktree_branch(slug)
|
|
209
|
+
|
|
210
|
+
# 快速恢复:检查 worktree 是否已注册
|
|
211
|
+
if worktree_path.exists():
|
|
212
|
+
code, _, _ = await _run_git(
|
|
213
|
+
"rev-parse", "--git-dir", cwd=worktree_path
|
|
214
|
+
)
|
|
215
|
+
if code == 0:
|
|
216
|
+
return WorktreeInfo(
|
|
217
|
+
slug=slug,
|
|
218
|
+
path=worktree_path,
|
|
219
|
+
branch=worktree_branch,
|
|
220
|
+
original_path=repo_path,
|
|
221
|
+
created_at=worktree_path.stat().st_mtime,
|
|
222
|
+
agent_id=agent_id,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 新 worktree:-B 重置之前移除留下的孤儿分支
|
|
226
|
+
code, _, stderr = await _run_git(
|
|
227
|
+
"worktree", "add", "-B", worktree_branch, str(worktree_path), "HEAD",
|
|
228
|
+
cwd=repo_path,
|
|
229
|
+
)
|
|
230
|
+
if code != 0:
|
|
231
|
+
raise RuntimeError(f"git worktree add failed: {stderr}")
|
|
232
|
+
|
|
233
|
+
# 符号链接公共目录
|
|
234
|
+
await _symlink_common_dirs(repo_path, worktree_path)
|
|
235
|
+
|
|
236
|
+
return WorktreeInfo(
|
|
237
|
+
slug=slug,
|
|
238
|
+
path=worktree_path,
|
|
239
|
+
branch=worktree_branch,
|
|
240
|
+
original_path=repo_path,
|
|
241
|
+
created_at=time.time(),
|
|
242
|
+
agent_id=agent_id,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
async def remove_worktree(self, slug: str) -> bool:
|
|
246
|
+
"""按 slug 移除 worktree。
|
|
247
|
+
|
|
248
|
+
首先清理符号链接,然后运行 ``git worktree remove --force``。
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
如果 worktree 被移除返回 True;如果不存在则返回 False。
|
|
252
|
+
"""
|
|
253
|
+
validate_worktree_slug(slug)
|
|
254
|
+
flat_slug = _flatten_slug(slug)
|
|
255
|
+
worktree_path = self.base_dir / flat_slug
|
|
256
|
+
|
|
257
|
+
if not worktree_path.exists():
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# 在 git 移除目录之前先移除符号链接
|
|
261
|
+
await _remove_symlinks(worktree_path)
|
|
262
|
+
|
|
263
|
+
# 从 worktree 的 git 元数据确定仓库根目录
|
|
264
|
+
code, git_common, _ = await _run_git(
|
|
265
|
+
"rev-parse", "--git-common-dir", cwd=worktree_path
|
|
266
|
+
)
|
|
267
|
+
if code == 0 and git_common:
|
|
268
|
+
# git_common 指向主仓库内的 .git
|
|
269
|
+
repo_path = Path(git_common).resolve().parent
|
|
270
|
+
if repo_path.exists():
|
|
271
|
+
await _run_git(
|
|
272
|
+
"worktree", "remove", "--force", str(worktree_path),
|
|
273
|
+
cwd=repo_path,
|
|
274
|
+
)
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
# 回退:尝试从任何工作目录通过绝对路径移除
|
|
278
|
+
# 如果 repo_path 检测失败,尝试使用 cwd=base_dir 移除
|
|
279
|
+
code, _, _ = await _run_git(
|
|
280
|
+
"worktree", "remove", "--force", str(worktree_path),
|
|
281
|
+
cwd=self.base_dir,
|
|
282
|
+
)
|
|
283
|
+
return code == 0
|
|
284
|
+
|
|
285
|
+
async def list_worktrees(self) -> list[WorktreeInfo]:
|
|
286
|
+
"""返回 base_dir 下每个已知 worktree 的 WorktreeInfo。"""
|
|
287
|
+
if not self.base_dir.exists():
|
|
288
|
+
return []
|
|
289
|
+
|
|
290
|
+
results: list[WorktreeInfo] = []
|
|
291
|
+
for child in self.base_dir.iterdir():
|
|
292
|
+
if not child.is_dir():
|
|
293
|
+
continue
|
|
294
|
+
code, _, _ = await _run_git("rev-parse", "--git-dir", cwd=child)
|
|
295
|
+
if code != 0:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# 从 HEAD 恢复分支名称
|
|
299
|
+
rc, branch_out, _ = await _run_git(
|
|
300
|
+
"rev-parse", "--abbrev-ref", "HEAD", cwd=child
|
|
301
|
+
)
|
|
302
|
+
branch = branch_out if rc == 0 else "unknown"
|
|
303
|
+
|
|
304
|
+
# 从 git-common-dir 恢复原始仓库路径
|
|
305
|
+
rc2, common_dir, _ = await _run_git(
|
|
306
|
+
"rev-parse", "--git-common-dir", cwd=child
|
|
307
|
+
)
|
|
308
|
+
if rc2 == 0 and common_dir:
|
|
309
|
+
original_path = Path(common_dir).resolve().parent
|
|
310
|
+
else:
|
|
311
|
+
original_path = child
|
|
312
|
+
|
|
313
|
+
# Slug 是目录名(扁平形式);从 '+' 恢复 '/'
|
|
314
|
+
slug = child.name.replace("+", "/")
|
|
315
|
+
results.append(
|
|
316
|
+
WorktreeInfo(
|
|
317
|
+
slug=slug,
|
|
318
|
+
path=child,
|
|
319
|
+
branch=branch,
|
|
320
|
+
original_path=original_path,
|
|
321
|
+
created_at=child.stat().st_mtime,
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return results
|
|
326
|
+
|
|
327
|
+
async def cleanup_stale(self, active_agent_ids: set[str] | None = None) -> list[str]:
|
|
328
|
+
"""移除没有活跃代理的 worktree。
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
active_agent_ids: 仍在运行的代理 ID 集合。如果为 None,
|
|
332
|
+
*所有* 有 agent_id 的 worktree 都被视为过期。
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
已移除的 slugs 列表。
|
|
336
|
+
"""
|
|
337
|
+
worktrees = await self.list_worktrees()
|
|
338
|
+
removed: list[str] = []
|
|
339
|
+
for info in worktrees:
|
|
340
|
+
if info.agent_id is None:
|
|
341
|
+
continue
|
|
342
|
+
if active_agent_ids is not None and info.agent_id in active_agent_ids:
|
|
343
|
+
continue
|
|
344
|
+
ok = await self.remove_worktree(info.slug)
|
|
345
|
+
if ok:
|
|
346
|
+
removed.append(info.slug)
|
|
347
|
+
return removed
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
任务模块导出
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
本模块导出 tasks 子目录中的公共接口。
|
|
6
|
+
|
|
7
|
+
导出内容:
|
|
8
|
+
- BackgroundTaskManager: 后台任务管理器
|
|
9
|
+
- TaskRecord: 任务记录
|
|
10
|
+
- TaskStatus: 任务状态类型
|
|
11
|
+
- TaskType: 任务类型
|
|
12
|
+
- get_task_manager: 获取任务管理器
|
|
13
|
+
- spawn_local_agent_task: 启动本地 agent 任务
|
|
14
|
+
- spawn_shell_task: 启动 shell 任务
|
|
15
|
+
- stop_task: 停止任务
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from illusion.tasks.local_agent_task import spawn_local_agent_task
|
|
19
|
+
from illusion.tasks.local_shell_task import spawn_shell_task
|
|
20
|
+
from illusion.tasks.manager import BackgroundTaskManager, get_task_manager
|
|
21
|
+
from illusion.tasks.stop_task import stop_task
|
|
22
|
+
from illusion.tasks.types import TaskRecord, TaskStatus, TaskType
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BackgroundTaskManager",
|
|
26
|
+
"TaskRecord",
|
|
27
|
+
"TaskStatus",
|
|
28
|
+
"TaskType",
|
|
29
|
+
"get_task_manager",
|
|
30
|
+
"spawn_local_agent_task",
|
|
31
|
+
"spawn_shell_task",
|
|
32
|
+
"stop_task",
|
|
33
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
本地 Agent 任务外观模块
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
本模块提供本地 agent 子进程任务的简单接口。
|
|
6
|
+
|
|
7
|
+
使用示例:
|
|
8
|
+
>>> from illusion.tasks.local_agent_task import spawn_local_agent_task
|
|
9
|
+
>>> # 启动本地 agent 任务
|
|
10
|
+
>>> record = await spawn_local_agent_task(
|
|
11
|
+
... prompt="帮我写一个 Hello World 程序",
|
|
12
|
+
... description="编写程序",
|
|
13
|
+
... cwd="."
|
|
14
|
+
... )
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from illusion.tasks.manager import get_task_manager
|
|
22
|
+
from illusion.tasks.types import TaskRecord
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def spawn_local_agent_task(
|
|
26
|
+
*,
|
|
27
|
+
prompt: str,
|
|
28
|
+
description: str,
|
|
29
|
+
cwd: str | Path,
|
|
30
|
+
model: str | None = None,
|
|
31
|
+
api_key: str | None = None,
|
|
32
|
+
command: str | None = None,
|
|
33
|
+
) -> TaskRecord:
|
|
34
|
+
"""启动本地 agent 子进程任务。"""
|
|
35
|
+
return await get_task_manager().create_agent_task(
|
|
36
|
+
prompt=prompt,
|
|
37
|
+
description=description,
|
|
38
|
+
cwd=cwd,
|
|
39
|
+
model=model,
|
|
40
|
+
api_key=api_key,
|
|
41
|
+
command=command,
|
|
42
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
本地 Shell 任务外观模块
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
本模块提供本地 shell 任务的简单接口。
|
|
6
|
+
|
|
7
|
+
使用示例:
|
|
8
|
+
>>> from illusion.tasks.local_shell_task import spawn_shell_task
|
|
9
|
+
>>> # 启动本地 shell 任务
|
|
10
|
+
>>> record = await spawn_shell_task("ls -la", "列出文件", ".")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from illusion.tasks.manager import get_task_manager
|
|
18
|
+
from illusion.tasks.types import TaskRecord
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def spawn_shell_task(command: str, description: str, cwd: str | Path) -> TaskRecord:
|
|
22
|
+
"""启动本地 shell 任务。"""
|
|
23
|
+
return await get_task_manager().create_shell_task(
|
|
24
|
+
command=command,
|
|
25
|
+
description=description,
|
|
26
|
+
cwd=cwd,
|
|
27
|
+
)
|