升级 Trellis 并完成项目审查(07-26-trellis-upgrade-project-audit)
这个提交包含在:
@@ -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
|
||||
|
||||
在新工单中引用
屏蔽一个用户