升级 Trellis 并完成项目审查(07-26-trellis-upgrade-project-audit)

这个提交包含在:
2026-07-26 18:10:49 +08:00
父节点 b3dadfa1bf
当前提交 f223f559ad
共修改 77 个文件,包含 5850 行新增和 1327 行删除
+178 -39
查看文件
@@ -1,7 +1,7 @@
"""
CLI Adapter for Multi-Platform Support.
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces.
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Devin, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces.
Supported platforms:
- claude: Claude Code (default)
@@ -13,12 +13,16 @@ Supported platforms:
- kiro: Kiro Code (skills-based)
- gemini: Gemini CLI
- antigravity: Antigravity (workflow-based)
- windsurf: Windsurf (workflow-based)
- devin: Devin (formerly Windsurf; workflow-based)
- qoder: Qoder
- codebuddy: CodeBuddy
- copilot: GitHub Copilot (VS Code)
- droid: Factory Droid (commands-based)
- pi: Pi Agent (extension-backed)
- trae: Trae IDE (IDE-only, hooks-based)
- omp: Oh My Pi
- grok: Grok Build (pull-based skills/agents; no hook context injection)
- kimi: Kimi Code (pull-based skills; commands delivered as skills; no hook context injection)
Usage:
from common.cli_adapter import CLIAdapter
@@ -47,12 +51,16 @@ Platform = Literal[
"kiro",
"gemini",
"antigravity",
"windsurf",
"devin",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
"trae",
"omp",
"grok",
"kimi",
]
@@ -97,7 +105,7 @@ class CLIAdapter:
"""Get platform-specific config directory name.
Returns:
Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.windsurf', '.qoder', '.codebuddy', '.github/copilot', '.factory', or '.pi')
Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.devin', '.qoder', '.codebuddy', '.github/copilot', '.factory', '.pi', or '.trae')
"""
if self.platform == "opencode":
return ".opencode"
@@ -115,8 +123,8 @@ class CLIAdapter:
return ".gemini"
elif self.platform == "antigravity":
return ".agent"
elif self.platform == "windsurf":
return ".windsurf"
elif self.platform == "devin":
return ".devin"
elif self.platform == "qoder":
return ".qoder"
elif self.platform == "codebuddy":
@@ -127,6 +135,14 @@ class CLIAdapter:
return ".factory"
elif self.platform == "pi":
return ".pi"
elif self.platform == "trae":
return ".trae"
elif self.platform == "omp":
return ".omp"
elif self.platform == "grok":
return ".grok"
elif self.platform == "kimi":
return ".kimi-code"
else:
return ".claude"
@@ -137,7 +153,7 @@ class CLIAdapter:
project_root: Project root directory
Returns:
Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .windsurf, .qoder, .codebuddy, .github/copilot, .factory, or .pi)
Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .devin, .qoder, .codebuddy, .github/copilot, .factory, .pi, or .trae)
"""
return project_root / self.config_dir_name
@@ -169,7 +185,7 @@ class CLIAdapter:
Note:
Cursor uses prefix naming: .cursor/commands/trellis-<name>.md
Antigravity uses workflow directory: .agent/workflows/<name>.md
Windsurf uses workflow directory: .windsurf/workflows/trellis-<name>.md
Devin uses workflow directory: .devin/workflows/trellis-<name>.md
Copilot uses prompt files: .github/prompts/<name>.prompt.md
Pi uses prompt templates: .pi/prompts/trellis-<name>.md
Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md
@@ -184,8 +200,31 @@ class CLIAdapter:
filename = filename[:-3]
return prompts_dir / f"trellis-{filename}.md"
return prompts_dir / Path(*parts)
# OMP and Grok: flat slash commands under .{platform}/commands/trellis-<name>.md
if self.platform in ("omp", "grok"):
commands_dir = self.get_config_dir(project_root) / "commands"
if not parts:
return commands_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
if filename.endswith(".md"):
filename = filename[:-3]
return commands_dir / f"trellis-{filename}.md"
return commands_dir / Path(*parts)
if self.platform == "windsurf":
# Kimi: commands are skills under .kimi-code/skills/trellis-<name>/SKILL.md
if self.platform == "kimi":
skills_dir = self.get_config_dir(project_root) / "skills"
if not parts:
return skills_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
if filename.endswith(".md"):
filename = filename[:-3]
return skills_dir / f"trellis-{filename}" / "SKILL.md"
return skills_dir / Path(*parts)
if self.platform == "devin":
workflow_dir = self.get_config_dir(project_root) / "workflows"
if not parts:
return workflow_dir
@@ -242,7 +281,7 @@ class CLIAdapter:
Kiro: .kiro/skills/trellis-<name>/SKILL.md
Gemini: .gemini/commands/trellis/<name>.toml
Antigravity: .agent/workflows/<name>.md
Windsurf: .windsurf/workflows/trellis-<name>.md
Devin: .devin/workflows/trellis-<name>.md
Pi: .pi/prompts/trellis-<name>.md
Others: .{platform}/commands/trellis/<name>.md
"""
@@ -258,8 +297,8 @@ class CLIAdapter:
return f".gemini/commands/trellis/{name}.toml"
elif self.platform == "antigravity":
return f".agent/workflows/{name}.md"
elif self.platform == "windsurf":
return f".windsurf/workflows/trellis-{name}.md"
elif self.platform == "devin":
return f".devin/workflows/trellis-{name}.md"
elif self.platform == "kilo":
return f".kilocode/workflows/{name}.md"
elif self.platform == "copilot":
@@ -268,6 +307,10 @@ class CLIAdapter:
return f".factory/commands/trellis/{name}.md"
elif self.platform == "pi":
return f".pi/prompts/trellis-{name}.md"
elif self.platform in ("omp", "grok"):
return f"{self.config_dir_name}/commands/trellis-{name}.md"
elif self.platform == "kimi":
return f".kimi-code/skills/trellis-{name}/SKILL.md"
else:
return f"{self.config_dir_name}/commands/trellis/{name}.md"
@@ -293,7 +336,7 @@ class CLIAdapter:
return {} # Gemini CLI doesn't have a non-interactive env var
elif self.platform == "antigravity":
return {}
elif self.platform == "windsurf":
elif self.platform == "devin":
return {}
elif self.platform == "qoder":
return {}
@@ -305,6 +348,14 @@ class CLIAdapter:
return {}
elif self.platform == "pi":
return {}
elif self.platform == "trae":
return {}
elif self.platform == "omp":
return {}
elif self.platform == "grok":
return {}
elif self.platform == "kimi":
return {}
else:
return {"CLAUDE_NON_INTERACTIVE": "1"}
@@ -370,9 +421,9 @@ class CLIAdapter:
raise ValueError(
"Antigravity workflows are UI slash commands; CLI agent run is not supported."
)
elif self.platform == "windsurf":
elif self.platform == "devin":
raise ValueError(
"Windsurf workflows are UI slash commands; CLI agent run is not supported."
"Devin workflows are UI slash commands; CLI agent run is not supported."
)
elif self.platform == "qoder":
cmd = ["qodercli", "-p", prompt]
@@ -390,6 +441,21 @@ class CLIAdapter:
)
elif self.platform == "pi":
cmd = ["pi", "-p", prompt]
elif self.platform == "trae":
raise ValueError(
"Trae is IDE-only; CLI agent run is not supported."
)
elif self.platform == "omp":
raise ValueError(
"OMP uses native task tool for agent runs; CLI agent run is not supported."
)
elif self.platform == "grok":
# Headless single-prompt; sub-agents use in-process spawn_subagent.
cmd = ["grok", "-p", prompt, "--yolo"]
elif self.platform == "kimi":
# Headless single-prompt with auto-approval; sub-agents are the
# built-in coder/explore/plan agents dispatched in-session.
cmd = ["kimi", "-p", prompt, "--yolo"]
else: # claude
cmd = ["claude", "-p"]
@@ -436,9 +502,9 @@ class CLIAdapter:
raise ValueError(
"Antigravity workflows are UI slash commands; CLI resume is not supported."
)
elif self.platform == "windsurf":
elif self.platform == "devin":
raise ValueError(
"Windsurf workflows are UI slash commands; CLI resume is not supported."
"Devin workflows are UI slash commands; CLI resume is not supported."
)
elif self.platform == "qoder":
return ["qodercli", "--resume", session_id]
@@ -456,6 +522,18 @@ class CLIAdapter:
)
elif self.platform == "pi":
return ["pi", "-c", session_id]
elif self.platform == "trae":
raise ValueError(
"Trae is IDE-only; CLI resume is not supported."
)
elif self.platform == "omp":
raise ValueError(
"OMP uses native task tool for agent runs; CLI resume is not supported."
)
elif self.platform == "grok":
return ["grok", "-c"]
elif self.platform == "kimi":
return ["kimi", "--session", session_id]
else:
return ["claude", "--resume", session_id]
@@ -518,8 +596,8 @@ class CLIAdapter:
return "gemini"
elif self.platform == "antigravity":
return "agy"
elif self.platform == "windsurf":
return "windsurf"
elif self.platform == "devin":
return "devin"
elif self.platform == "qoder":
return "qodercli"
elif self.platform == "codebuddy":
@@ -530,6 +608,14 @@ class CLIAdapter:
return "droid"
elif self.platform == "pi":
return "pi"
elif self.platform == "trae":
return "trae"
elif self.platform == "omp":
return "omp"
elif self.platform == "grok":
return "grok"
elif self.platform == "kimi":
return "kimi"
else:
return "claude"
@@ -540,7 +626,15 @@ class CLIAdapter:
Claude Code, OpenCode, iFlow, and Codex support CLI agent execution.
Cursor is IDE-only and doesn't support CLI agents.
"""
return self.platform in ("claude", "opencode", "iflow", "codex", "pi")
return self.platform in (
"claude",
"opencode",
"iflow",
"codex",
"pi",
"grok",
"kimi",
)
@property
def requires_agent_definition_file(self) -> bool:
@@ -594,14 +688,21 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
"""Get CLI adapter for the specified platform.
Args:
platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')
platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or 'trae')
Returns:
CLIAdapter instance
Raises:
ValueError: If platform is not supported
Note:
'windsurf' is accepted as a deprecated alias for 'devin' (Windsurf was
renamed to Devin) and normalized before validation.
"""
# Deprecated alias: Windsurf was renamed to Devin.
if platform == "windsurf":
platform = "devin"
if platform not in (
"claude",
"opencode",
@@ -612,15 +713,19 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
"kiro",
"gemini",
"antigravity",
"windsurf",
"devin",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
"trae",
"omp",
"grok",
"kimi",
):
raise ValueError(
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')"
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', 'omp', 'grok', or 'kimi')"
)
return CLIAdapter(platform=platform) # type: ignore
@@ -636,18 +741,23 @@ _ALL_PLATFORM_CONFIG_DIRS = (
".kiro",
".gemini",
".agent",
".windsurf",
".devin",
".windsurf", # deprecated: pre-rename Devin config dir (still a platform signal)
".qoder",
".codebuddy",
".github/copilot",
".factory",
".pi",
".trae",
".omp",
".grok",
".kimi-code",
)
"""Platform-specific config directory names used by detect_platform exclusion
checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform
layer (written by Codex, also consumed by Amp/Cline/Warp/etc. via the
agentskills.io standard), not a single-platform signal. Its presence must not
block detection of Kiro, Antigravity, Windsurf, or other platforms."""
block detection of Kiro, Antigravity, Devin, or other platforms."""
def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool:
@@ -667,27 +777,33 @@ def detect_platform(project_root: Path) -> Platform:
2. .opencode directory exists → opencode
3. .iflow directory exists → iflow
4. .cursor directory exists (without .claude) → cursor
5. .codex exists and no other platform dirs → codex
6. .kilocode directory exists → kilo
7. .kiro/skills exists and no other platform dirs → kiro
8. .gemini directory exists → gemini
5. .gemini directory exists → gemini
6. .codex exists and no other platform dirs → codex
7. .kilocode directory exists → kilo
8. .kiro/skills exists and no other platform dirs → kiro
9. .agent/workflows exists and no other platform dirs → antigravity
10. .windsurf/workflows exists and no other platform dirs → windsurf
10. .devin/workflows (or legacy .windsurf/workflows) exists and no other platform dirs → devin
11. .codebuddy directory exists → codebuddy
12. .qoder directory exists → qoder
13. .pi directory exists → pi
14. Default → claude
13. .github/copilot directory exists → copilot
14. .factory directory exists → droid
15. .pi directory exists → pi
16. .trae directory exists → trae
17. Default → claude
Args:
project_root: Project root directory
Returns:
Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or default 'claude')
Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', or default 'claude')
"""
import os
# Check environment variable first
env_platform = os.environ.get("TRELLIS_PLATFORM", "").lower()
# Deprecated alias: Windsurf was renamed to Devin.
if env_platform == "windsurf":
env_platform = "devin"
if env_platform in (
"claude",
"opencode",
@@ -698,12 +814,16 @@ def detect_platform(project_root: Path) -> Platform:
"kiro",
"gemini",
"antigravity",
"windsurf",
"devin",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
"trae",
"omp",
"grok",
"kimi",
):
return env_platform # type: ignore
@@ -749,13 +869,16 @@ def detect_platform(project_root: Path) -> Platform:
):
return "antigravity"
# Check for Windsurf workflow directory only when no other platform config exists
# Check for Devin workflow directory only when no other platform config
# exists. `.windsurf/workflows` is the legacy pre-rename path (still detected
# as devin for back-compat until users migrate via `trellis update --migrate`).
if (
project_root / ".windsurf" / "workflows"
).is_dir() and not _has_other_platform_dir(
project_root, {".windsurf"}
(project_root / ".devin" / "workflows").is_dir()
or (project_root / ".windsurf" / "workflows").is_dir()
) and not _has_other_platform_dir(
project_root, {".devin", ".windsurf"}
):
return "windsurf"
return "devin"
# Check for .codebuddy directory (CodeBuddy-specific)
if (project_root / ".codebuddy").is_dir():
@@ -777,6 +900,22 @@ def detect_platform(project_root: Path) -> Platform:
if (project_root / ".pi").is_dir():
return "pi"
# Check for .trae directory (Trae IDE-specific)
if (project_root / ".trae").is_dir():
return "trae"
# Check for .omp directory (OMP-specific)
if (project_root / ".omp").is_dir():
return "omp"
# Check for .grok directory (Grok Build-specific)
if (project_root / ".grok").is_dir():
return "grok"
# Check for .kimi-code directory (Kimi Code-specific)
if (project_root / ".kimi-code").is_dir():
return "kimi"
# Fallback: checkout only has the Codex shared-skills layer
# (.agents/skills/trellis-* dirs) and no explicit platform config dir.
# Happens on fresh clones where .codex/ is gitignored/absent but the