deepwork 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.
- deepwork/__init__.py +25 -0
- deepwork/cli/__init__.py +1 -0
- deepwork/cli/install.py +290 -0
- deepwork/cli/main.py +25 -0
- deepwork/cli/sync.py +176 -0
- deepwork/core/__init__.py +1 -0
- deepwork/core/adapters.py +373 -0
- deepwork/core/detector.py +93 -0
- deepwork/core/generator.py +290 -0
- deepwork/core/hooks_syncer.py +206 -0
- deepwork/core/parser.py +310 -0
- deepwork/core/policy_parser.py +285 -0
- deepwork/hooks/__init__.py +1 -0
- deepwork/hooks/evaluate_policies.py +159 -0
- deepwork/schemas/__init__.py +1 -0
- deepwork/schemas/job_schema.py +212 -0
- deepwork/schemas/policy_schema.py +68 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +102 -0
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +359 -0
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +435 -0
- deepwork/standard_jobs/deepwork_jobs/steps/refine.md +447 -0
- deepwork/standard_jobs/deepwork_policy/hooks/capture_work_tree.sh +26 -0
- deepwork/standard_jobs/deepwork_policy/hooks/get_changed_files.sh +30 -0
- deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +8 -0
- deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +72 -0
- deepwork/standard_jobs/deepwork_policy/hooks/user_prompt_submit.sh +17 -0
- deepwork/standard_jobs/deepwork_policy/job.yml +35 -0
- deepwork/standard_jobs/deepwork_policy/steps/define.md +174 -0
- deepwork/templates/__init__.py +1 -0
- deepwork/templates/claude/command-job-step.md.jinja +210 -0
- deepwork/templates/gemini/command-job-step.toml.jinja +169 -0
- deepwork/utils/__init__.py +1 -0
- deepwork/utils/fs.py +128 -0
- deepwork/utils/git.py +164 -0
- deepwork/utils/validation.py +31 -0
- deepwork/utils/yaml_utils.py +89 -0
- deepwork-0.1.0.dist-info/METADATA +389 -0
- deepwork-0.1.0.dist-info/RECORD +41 -0
- deepwork-0.1.0.dist-info/WHEEL +4 -0
- deepwork-0.1.0.dist-info/entry_points.txt +2 -0
- deepwork-0.1.0.dist-info/licenses/LICENSE.md +60 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Agent adapters for AI coding assistants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdapterError(Exception):
|
|
13
|
+
"""Exception raised for adapter errors."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CommandLifecycleHook(str, Enum):
|
|
19
|
+
"""Generic command lifecycle hook events supported by DeepWork.
|
|
20
|
+
|
|
21
|
+
These represent hook points in the AI agent's command execution lifecycle.
|
|
22
|
+
Each adapter maps these generic names to platform-specific event names.
|
|
23
|
+
The enum values are the generic names used in job.yml files.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Triggered after the agent finishes responding (before returning to user)
|
|
27
|
+
# Use for quality validation loops, output verification
|
|
28
|
+
AFTER_AGENT = "after_agent"
|
|
29
|
+
|
|
30
|
+
# Triggered before the agent uses a tool
|
|
31
|
+
# Use for tool-specific validation or pre-processing
|
|
32
|
+
BEFORE_TOOL = "before_tool"
|
|
33
|
+
|
|
34
|
+
# Triggered when the user submits a new prompt
|
|
35
|
+
# Use for session initialization, context setup
|
|
36
|
+
BEFORE_PROMPT = "before_prompt"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# List of all supported command lifecycle hooks
|
|
40
|
+
COMMAND_LIFECYCLE_HOOKS_SUPPORTED: list[CommandLifecycleHook] = list(CommandLifecycleHook)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AgentAdapter(ABC):
|
|
44
|
+
"""Base class for AI agent platform adapters.
|
|
45
|
+
|
|
46
|
+
Subclasses are automatically registered when defined, enabling dynamic
|
|
47
|
+
discovery of supported platforms.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Class-level registry for auto-discovery
|
|
51
|
+
_registry: ClassVar[dict[str, type[AgentAdapter]]] = {}
|
|
52
|
+
|
|
53
|
+
# Platform configuration (subclasses define as class attributes)
|
|
54
|
+
name: ClassVar[str]
|
|
55
|
+
display_name: ClassVar[str]
|
|
56
|
+
config_dir: ClassVar[str]
|
|
57
|
+
commands_dir: ClassVar[str] = "commands"
|
|
58
|
+
command_template: ClassVar[str] = "command-job-step.md.jinja"
|
|
59
|
+
|
|
60
|
+
# Mapping from generic CommandLifecycleHook to platform-specific event names.
|
|
61
|
+
# Subclasses should override this to provide platform-specific mappings.
|
|
62
|
+
hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
|
|
63
|
+
|
|
64
|
+
def __init__(self, project_root: Path | str | None = None):
|
|
65
|
+
"""
|
|
66
|
+
Initialize adapter with optional project root.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
project_root: Path to project root directory
|
|
70
|
+
"""
|
|
71
|
+
self.project_root = Path(project_root) if project_root else None
|
|
72
|
+
|
|
73
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
74
|
+
"""Auto-register subclasses."""
|
|
75
|
+
super().__init_subclass__(**kwargs)
|
|
76
|
+
# Only register if the class has a name attribute set (not inherited default)
|
|
77
|
+
if "name" in cls.__dict__ and cls.name:
|
|
78
|
+
AgentAdapter._registry[cls.name] = cls
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_all(cls) -> dict[str, type[AgentAdapter]]:
|
|
82
|
+
"""
|
|
83
|
+
Return all registered adapter classes.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dict mapping adapter names to adapter classes
|
|
87
|
+
"""
|
|
88
|
+
return cls._registry.copy()
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get(cls, name: str) -> type[AgentAdapter]:
|
|
92
|
+
"""
|
|
93
|
+
Get adapter class by name.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Adapter name (e.g., "claude", "gemini", "copilot")
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Adapter class
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
AdapterError: If adapter name is not registered
|
|
103
|
+
"""
|
|
104
|
+
if name not in cls._registry:
|
|
105
|
+
raise AdapterError(
|
|
106
|
+
f"Unknown adapter '{name}'. Supported adapters: {', '.join(cls._registry.keys())}"
|
|
107
|
+
)
|
|
108
|
+
return cls._registry[name]
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def list_names(cls) -> list[str]:
|
|
112
|
+
"""
|
|
113
|
+
List all registered adapter names.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of adapter names
|
|
117
|
+
"""
|
|
118
|
+
return list(cls._registry.keys())
|
|
119
|
+
|
|
120
|
+
def get_template_dir(self, templates_root: Path) -> Path:
|
|
121
|
+
"""
|
|
122
|
+
Get the template directory for this adapter.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
templates_root: Root directory containing platform templates
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Path to this adapter's template directory
|
|
129
|
+
"""
|
|
130
|
+
return templates_root / self.name
|
|
131
|
+
|
|
132
|
+
def get_commands_dir(self, project_root: Path | None = None) -> Path:
|
|
133
|
+
"""
|
|
134
|
+
Get the commands directory path.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
project_root: Project root (uses instance's project_root if not provided)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Path to commands directory
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
AdapterError: If no project root specified
|
|
144
|
+
"""
|
|
145
|
+
root = project_root or self.project_root
|
|
146
|
+
if not root:
|
|
147
|
+
raise AdapterError("No project root specified")
|
|
148
|
+
return root / self.config_dir / self.commands_dir
|
|
149
|
+
|
|
150
|
+
def get_command_filename(self, job_name: str, step_id: str) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Get the filename for a command.
|
|
153
|
+
|
|
154
|
+
Can be overridden for different file formats (e.g., TOML for Gemini).
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
job_name: Name of the job
|
|
158
|
+
step_id: ID of the step
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Command filename (e.g., "job_name.step_id.md")
|
|
162
|
+
"""
|
|
163
|
+
return f"{job_name}.{step_id}.md"
|
|
164
|
+
|
|
165
|
+
def detect(self, project_root: Path | None = None) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Check if this platform is available in the project.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
project_root: Project root (uses instance's project_root if not provided)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if platform config directory exists
|
|
174
|
+
"""
|
|
175
|
+
root = project_root or self.project_root
|
|
176
|
+
if not root:
|
|
177
|
+
return False
|
|
178
|
+
config_path = root / self.config_dir
|
|
179
|
+
return config_path.exists() and config_path.is_dir()
|
|
180
|
+
|
|
181
|
+
def get_platform_hook_name(self, hook: CommandLifecycleHook) -> str | None:
|
|
182
|
+
"""
|
|
183
|
+
Get the platform-specific event name for a generic hook.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
hook: Generic CommandLifecycleHook
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Platform-specific event name, or None if not supported
|
|
190
|
+
"""
|
|
191
|
+
return self.hook_name_mapping.get(hook)
|
|
192
|
+
|
|
193
|
+
def supports_hook(self, hook: CommandLifecycleHook) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Check if this adapter supports a specific hook.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
hook: Generic CommandLifecycleHook
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if the hook is supported
|
|
202
|
+
"""
|
|
203
|
+
return hook in self.hook_name_mapping
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
|
|
207
|
+
"""
|
|
208
|
+
Sync hooks to platform settings.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
project_path: Path to project root
|
|
212
|
+
hooks: Dict mapping lifecycle events to hook configurations
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Number of hooks synced
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
AdapterError: If sync fails
|
|
219
|
+
"""
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _hook_already_present(hooks: list[dict[str, Any]], script_path: str) -> bool:
|
|
224
|
+
"""Check if a hook with the given script path is already in the list."""
|
|
225
|
+
for hook in hooks:
|
|
226
|
+
hook_list = hook.get("hooks", [])
|
|
227
|
+
for h in hook_list:
|
|
228
|
+
if h.get("command") == script_path:
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# =============================================================================
|
|
234
|
+
# Platform Adapters
|
|
235
|
+
# =============================================================================
|
|
236
|
+
#
|
|
237
|
+
# Each adapter must define hook_name_mapping to indicate which hooks it supports.
|
|
238
|
+
# Use an empty dict {} for platforms that don't support command-level hooks.
|
|
239
|
+
#
|
|
240
|
+
# Hook support reviewed:
|
|
241
|
+
# - Claude Code: Full support (Stop, PreToolUse, UserPromptSubmit) - 2025-01
|
|
242
|
+
# - Gemini CLI: No command-level hooks (reviewed 2026-01-12)
|
|
243
|
+
# Gemini's hooks are global/project-level in settings.json, not per-command.
|
|
244
|
+
# TOML command files only support 'prompt' and 'description' fields.
|
|
245
|
+
# See: doc/platforms/gemini/hooks_system.md
|
|
246
|
+
# =============================================================================
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class ClaudeAdapter(AgentAdapter):
|
|
250
|
+
"""Adapter for Claude Code."""
|
|
251
|
+
|
|
252
|
+
name = "claude"
|
|
253
|
+
display_name = "Claude Code"
|
|
254
|
+
config_dir = ".claude"
|
|
255
|
+
|
|
256
|
+
# Claude Code uses PascalCase event names
|
|
257
|
+
hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {
|
|
258
|
+
CommandLifecycleHook.AFTER_AGENT: "Stop",
|
|
259
|
+
CommandLifecycleHook.BEFORE_TOOL: "PreToolUse",
|
|
260
|
+
CommandLifecycleHook.BEFORE_PROMPT: "UserPromptSubmit",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
|
|
264
|
+
"""
|
|
265
|
+
Sync hooks to Claude Code settings.json.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
project_path: Path to project root
|
|
269
|
+
hooks: Merged hooks configuration
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Number of hooks synced
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
AdapterError: If sync fails
|
|
276
|
+
"""
|
|
277
|
+
if not hooks:
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
settings_file = project_path / self.config_dir / "settings.json"
|
|
281
|
+
|
|
282
|
+
# Load existing settings or create new
|
|
283
|
+
existing_settings: dict[str, Any] = {}
|
|
284
|
+
if settings_file.exists():
|
|
285
|
+
try:
|
|
286
|
+
with open(settings_file, encoding="utf-8") as f:
|
|
287
|
+
existing_settings = json.load(f)
|
|
288
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
289
|
+
raise AdapterError(f"Failed to read settings.json: {e}") from e
|
|
290
|
+
|
|
291
|
+
# Merge hooks into existing settings
|
|
292
|
+
if "hooks" not in existing_settings:
|
|
293
|
+
existing_settings["hooks"] = {}
|
|
294
|
+
|
|
295
|
+
for event, event_hooks in hooks.items():
|
|
296
|
+
if event not in existing_settings["hooks"]:
|
|
297
|
+
existing_settings["hooks"][event] = []
|
|
298
|
+
|
|
299
|
+
# Add new hooks that aren't already present
|
|
300
|
+
for hook in event_hooks:
|
|
301
|
+
script_path = hook.get("hooks", [{}])[0].get("command", "")
|
|
302
|
+
if not _hook_already_present(existing_settings["hooks"][event], script_path):
|
|
303
|
+
existing_settings["hooks"][event].append(hook)
|
|
304
|
+
|
|
305
|
+
# Write back to settings.json
|
|
306
|
+
try:
|
|
307
|
+
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
with open(settings_file, "w", encoding="utf-8") as f:
|
|
309
|
+
json.dump(existing_settings, f, indent=2)
|
|
310
|
+
except OSError as e:
|
|
311
|
+
raise AdapterError(f"Failed to write settings.json: {e}") from e
|
|
312
|
+
|
|
313
|
+
# Count total hooks
|
|
314
|
+
total = sum(len(hooks_list) for hooks_list in hooks.values())
|
|
315
|
+
return total
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class GeminiAdapter(AgentAdapter):
|
|
319
|
+
"""Adapter for Gemini CLI.
|
|
320
|
+
|
|
321
|
+
Gemini CLI uses TOML format for custom commands stored in .gemini/commands/.
|
|
322
|
+
Commands use colon (:) for namespacing instead of dot (.).
|
|
323
|
+
|
|
324
|
+
Note: Gemini CLI does NOT support command-level hooks. Hooks are configured
|
|
325
|
+
globally in settings.json, not per-command. Therefore, hook_name_mapping
|
|
326
|
+
is empty and sync_hooks returns 0.
|
|
327
|
+
|
|
328
|
+
See: doc/platforms/gemini/hooks_system.md
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
name = "gemini"
|
|
332
|
+
display_name = "Gemini CLI"
|
|
333
|
+
config_dir = ".gemini"
|
|
334
|
+
command_template = "command-job-step.toml.jinja"
|
|
335
|
+
|
|
336
|
+
# Gemini CLI does NOT support command-level hooks
|
|
337
|
+
# Hooks are global/project-level in settings.json, not per-command
|
|
338
|
+
hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
|
|
339
|
+
|
|
340
|
+
def get_command_filename(self, job_name: str, step_id: str) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Get the filename for a Gemini command.
|
|
343
|
+
|
|
344
|
+
Gemini uses TOML files and colon namespacing via subdirectories.
|
|
345
|
+
For job "my_job" and step "step_one", creates: my_job/step_one.toml
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
job_name: Name of the job
|
|
349
|
+
step_id: ID of the step
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Command filename path (e.g., "my_job/step_one.toml")
|
|
353
|
+
"""
|
|
354
|
+
return f"{job_name}/{step_id}.toml"
|
|
355
|
+
|
|
356
|
+
def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
|
|
357
|
+
"""
|
|
358
|
+
Sync hooks to Gemini CLI settings.
|
|
359
|
+
|
|
360
|
+
Gemini CLI does not support command-level hooks. All hooks are
|
|
361
|
+
configured globally in settings.json. This method is a no-op
|
|
362
|
+
that always returns 0.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
project_path: Path to project root
|
|
366
|
+
hooks: Dict mapping lifecycle events to hook configurations (ignored)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
0 (Gemini does not support command-level hooks)
|
|
370
|
+
"""
|
|
371
|
+
# Gemini CLI does not support command-level hooks
|
|
372
|
+
# Hooks are configured globally in settings.json, not per-command
|
|
373
|
+
return 0
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Platform detection for AI coding assistants."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from deepwork.core.adapters import AdapterError, AgentAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DetectorError(Exception):
|
|
9
|
+
"""Exception raised for platform detection errors."""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlatformDetector:
|
|
15
|
+
"""Detects available AI coding platforms using registered adapters."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, project_root: Path | str):
|
|
18
|
+
"""
|
|
19
|
+
Initialize detector.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
project_root: Path to project root directory
|
|
23
|
+
"""
|
|
24
|
+
self.project_root = Path(project_root)
|
|
25
|
+
|
|
26
|
+
def detect_platform(self, platform_name: str) -> AgentAdapter | None:
|
|
27
|
+
"""
|
|
28
|
+
Check if a specific platform is available.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
platform_name: Platform name ("claude", "gemini", "copilot")
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
AgentAdapter instance if platform is available, None otherwise
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
DetectorError: If platform_name is not supported
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
adapter_cls = AgentAdapter.get(platform_name)
|
|
41
|
+
except AdapterError as e:
|
|
42
|
+
raise DetectorError(str(e)) from e
|
|
43
|
+
|
|
44
|
+
adapter = adapter_cls(self.project_root)
|
|
45
|
+
if adapter.detect():
|
|
46
|
+
return adapter
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def detect_all_platforms(self) -> list[AgentAdapter]:
|
|
51
|
+
"""
|
|
52
|
+
Detect all available platforms.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of available adapter instances
|
|
56
|
+
"""
|
|
57
|
+
available = []
|
|
58
|
+
for platform_name in AgentAdapter.list_names():
|
|
59
|
+
adapter = self.detect_platform(platform_name)
|
|
60
|
+
if adapter is not None:
|
|
61
|
+
available.append(adapter)
|
|
62
|
+
|
|
63
|
+
return available
|
|
64
|
+
|
|
65
|
+
def get_adapter(self, platform_name: str) -> AgentAdapter:
|
|
66
|
+
"""
|
|
67
|
+
Get an adapter instance for a platform (without checking availability).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
platform_name: Platform name
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
AgentAdapter instance
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
DetectorError: If platform_name is not supported
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
adapter_cls = AgentAdapter.get(platform_name)
|
|
80
|
+
except AdapterError as e:
|
|
81
|
+
raise DetectorError(str(e)) from e
|
|
82
|
+
|
|
83
|
+
return adapter_cls(self.project_root)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def list_supported_platforms() -> list[str]:
|
|
87
|
+
"""
|
|
88
|
+
List all supported platform names.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of platform names
|
|
92
|
+
"""
|
|
93
|
+
return AgentAdapter.list_names()
|