forge-cpp-mcp 0.2.1__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.
- forge_cpp_mcp-0.2.1.dist-info/METADATA +436 -0
- forge_cpp_mcp-0.2.1.dist-info/RECORD +83 -0
- forge_cpp_mcp-0.2.1.dist-info/WHEEL +4 -0
- forge_cpp_mcp-0.2.1.dist-info/entry_points.txt +2 -0
- forge_cpp_mcp-0.2.1.dist-info/licenses/LICENSE +21 -0
- forgemcp/__init__.py +5 -0
- forgemcp/assets/clangd-result.html +119 -0
- forgemcp/assets/cmake-build.html +111 -0
- forgemcp/assets/cmake-configure.html +111 -0
- forgemcp/assets/cmake-profiles.html +111 -0
- forgemcp/assets/cmake-test.html +111 -0
- forgemcp/assets/process-details.html +113 -0
- forgemcp/assets/process-overview.html +111 -0
- forgemcp/assets/toolsets.html +111 -0
- forgemcp/assets/workspace-file.html +119 -0
- forgemcp/assets/workspace-result.html +119 -0
- forgemcp/assets/workspace-search.html +119 -0
- forgemcp/assets/workspace-tree.html +119 -0
- forgemcp/assets.py +62 -0
- forgemcp/clangd/__init__.py +1 -0
- forgemcp/clangd/errors.py +29 -0
- forgemcp/clangd/models.py +94 -0
- forgemcp/clangd/service.py +1279 -0
- forgemcp/clangd/session.py +520 -0
- forgemcp/cmake/__init__.py +1 -0
- forgemcp/cmake/errors.py +5 -0
- forgemcp/cmake/profiles.py +95 -0
- forgemcp/cmake/service.py +895 -0
- forgemcp/completion.py +49 -0
- forgemcp/icons/clangd.LICENSE +219 -0
- forgemcp/icons/clangd.svg +25 -0
- forgemcp/icons/cmake.svg +9 -0
- forgemcp/icons/process.svg +4 -0
- forgemcp/icons/toolchain.svg +1 -0
- forgemcp/icons/workspace-edit.svg +1 -0
- forgemcp/icons/workspace-file.svg +1 -0
- forgemcp/icons/workspace-search.svg +1 -0
- forgemcp/icons/workspace.svg +4 -0
- forgemcp/markdown.py +308 -0
- forgemcp/process/__init__.py +21 -0
- forgemcp/process/errors.py +21 -0
- forgemcp/process/models.py +171 -0
- forgemcp/process/service.py +830 -0
- forgemcp/process/transcript.py +116 -0
- forgemcp/progress.py +40 -0
- forgemcp/server.py +196 -0
- forgemcp/toolchain/__init__.py +1 -0
- forgemcp/toolchain/discovery.py +25 -0
- forgemcp/toolchain/errors.py +41 -0
- forgemcp/toolchain/loader.py +48 -0
- forgemcp/toolchain/providers/__init__.py +1 -0
- forgemcp/toolchain/providers/system.py +52 -0
- forgemcp/toolchain/providers/user.py +93 -0
- forgemcp/toolchain/providers/visual_studio.py +202 -0
- forgemcp/toolchain/service.py +226 -0
- forgemcp/toolchain/spec.py +73 -0
- forgemcp/toolchain/tools/__init__.py +1 -0
- forgemcp/toolchain/tools/clang.py +61 -0
- forgemcp/toolchain/tools/clang_cl.py +61 -0
- forgemcp/toolchain/tools/clangd.py +280 -0
- forgemcp/toolchain/tools/clangxx.py +61 -0
- forgemcp/toolchain/tools/cmake.py +280 -0
- forgemcp/toolchain/tools/cppvsdbg.py +27 -0
- forgemcp/toolchain/tools/ctest.py +202 -0
- forgemcp/toolchain/tools/gcc.py +61 -0
- forgemcp/toolchain/tools/gdb.py +61 -0
- forgemcp/toolchain/tools/git.py +61 -0
- forgemcp/toolchain/tools/gxx.py +61 -0
- forgemcp/toolchain/tools/link.py +27 -0
- forgemcp/toolchain/tools/lld.py +61 -0
- forgemcp/toolchain/tools/lld_link.py +61 -0
- forgemcp/toolchain/tools/lldb_dap.py +27 -0
- forgemcp/toolchain/tools/make.py +61 -0
- forgemcp/toolchain/tools/msbuild.py +61 -0
- forgemcp/toolchain/tools/msvc.py +27 -0
- forgemcp/toolchain/tools/ninja.py +61 -0
- forgemcp/workspace/__init__.py +5 -0
- forgemcp/workspace/diff.py +181 -0
- forgemcp/workspace/errors.py +5 -0
- forgemcp/workspace/metadata.py +87 -0
- forgemcp/workspace/path.py +71 -0
- forgemcp/workspace/providers.py +25 -0
- forgemcp/workspace/service.py +1766 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Typed methods for ctest."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, Protocol, TypedDict
|
|
7
|
+
import xml.etree.ElementTree as ET
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from forgemcp.process.errors import ProcessError
|
|
12
|
+
from forgemcp.process.models import ProcessTimeout
|
|
13
|
+
from forgemcp.process.service import ProcessService
|
|
14
|
+
|
|
15
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
16
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
type Progress = Callable[[str], Awaitable[None]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestCase(BaseModel):
|
|
23
|
+
name: str
|
|
24
|
+
status: Literal["passed", "failed", "skipped", "not_run"]
|
|
25
|
+
duration_seconds: float | None = None
|
|
26
|
+
message: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestResult(BaseModel):
|
|
30
|
+
return_code: int
|
|
31
|
+
process_id: int
|
|
32
|
+
tests: list[TestCase] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_report(path: Path) -> list[TestCase]:
|
|
36
|
+
if not path.is_file():
|
|
37
|
+
raise ToolParserError("CTest did not produce a JUnit report.")
|
|
38
|
+
try:
|
|
39
|
+
data = path.read_text(encoding="utf-8")
|
|
40
|
+
root = ET.fromstring(data)
|
|
41
|
+
results = []
|
|
42
|
+
for case in root.iter("testcase"):
|
|
43
|
+
failure = case.find("failure")
|
|
44
|
+
error = case.find("error")
|
|
45
|
+
skipped = case.find("skipped")
|
|
46
|
+
status = "passed"
|
|
47
|
+
message = None
|
|
48
|
+
if failure is not None or error is not None:
|
|
49
|
+
status = "failed"
|
|
50
|
+
problem = failure if failure is not None else error
|
|
51
|
+
message = problem.get("message") or problem.text or "Test failed."
|
|
52
|
+
elif skipped is not None:
|
|
53
|
+
status = "skipped"
|
|
54
|
+
message = skipped.get("message") or skipped.text
|
|
55
|
+
elif case.get("status") in ("notrun", "disabled"):
|
|
56
|
+
status = "not_run"
|
|
57
|
+
elapsed = case.get("time")
|
|
58
|
+
results.append(
|
|
59
|
+
TestCase(
|
|
60
|
+
name=case.get("name", ""),
|
|
61
|
+
status=status,
|
|
62
|
+
duration_seconds=float(elapsed) if elapsed is not None else None,
|
|
63
|
+
message=message,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return results
|
|
67
|
+
except (ET.ParseError, ValueError, OSError) as error:
|
|
68
|
+
raise ToolParserError("Cannot parse the CTest JUnit report.") from error
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Test(Protocol):
|
|
72
|
+
async def __call__(
|
|
73
|
+
self,
|
|
74
|
+
source: Path,
|
|
75
|
+
build_directory: Path | None,
|
|
76
|
+
*,
|
|
77
|
+
report_path: Path,
|
|
78
|
+
preset: str | None = None,
|
|
79
|
+
configuration: str | None = None,
|
|
80
|
+
names: Sequence[str] | None = None,
|
|
81
|
+
parallel: int | None = None,
|
|
82
|
+
timeout: ProcessTimeout = ProcessTimeout(total=600),
|
|
83
|
+
on_progress: Progress | None = None,
|
|
84
|
+
) -> TestResult: ...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Methods(TypedDict):
|
|
88
|
+
version: Callable[[], Awaitable[str]]
|
|
89
|
+
test: Test
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_spec(
|
|
93
|
+
path: Path,
|
|
94
|
+
processes: ProcessService,
|
|
95
|
+
environment: Mapping[str, str] | None = None,
|
|
96
|
+
inherit_environment: bool = True,
|
|
97
|
+
) -> ToolSpec:
|
|
98
|
+
path = path.resolve()
|
|
99
|
+
|
|
100
|
+
async def version() -> str:
|
|
101
|
+
output = {"stdout": "", "stderr": ""}
|
|
102
|
+
try:
|
|
103
|
+
async with await processes.launch(
|
|
104
|
+
path,
|
|
105
|
+
('--version',),
|
|
106
|
+
env=environment,
|
|
107
|
+
inherit_environment=inherit_environment,
|
|
108
|
+
timeout=ProcessTimeout(total=15),
|
|
109
|
+
) as session:
|
|
110
|
+
await session.close_stdin()
|
|
111
|
+
async for chunk in session.output():
|
|
112
|
+
# Only the short version banner is needed; drain the rest.
|
|
113
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
114
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
115
|
+
code = await session.wait()
|
|
116
|
+
if code != 0:
|
|
117
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
118
|
+
except ProcessError as error:
|
|
119
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
120
|
+
for text in output.values():
|
|
121
|
+
if match := re.search(
|
|
122
|
+
'ctest version ([0-9][0-9A-Za-z.+~-]*)',
|
|
123
|
+
text,
|
|
124
|
+
re.IGNORECASE | re.MULTILINE,
|
|
125
|
+
):
|
|
126
|
+
return match.group(1)
|
|
127
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
128
|
+
|
|
129
|
+
async def test(
|
|
130
|
+
source: Path,
|
|
131
|
+
build_directory: Path | None,
|
|
132
|
+
*,
|
|
133
|
+
report_path: Path,
|
|
134
|
+
preset: str | None = None,
|
|
135
|
+
configuration: str | None = None,
|
|
136
|
+
names: Sequence[str] | None = None,
|
|
137
|
+
parallel: int | None = None,
|
|
138
|
+
timeout: ProcessTimeout = ProcessTimeout(total=600),
|
|
139
|
+
on_progress: Progress | None = None,
|
|
140
|
+
) -> TestResult:
|
|
141
|
+
if preset is None and build_directory is None:
|
|
142
|
+
raise ToolCommandError("A plain test run requires a build directory.")
|
|
143
|
+
arguments = (
|
|
144
|
+
["--test-dir", str(build_directory)]
|
|
145
|
+
if preset is None
|
|
146
|
+
else ["--preset", preset]
|
|
147
|
+
)
|
|
148
|
+
arguments.extend(("--output-junit", str(report_path), "--output-on-failure"))
|
|
149
|
+
if preset is None:
|
|
150
|
+
arguments.append("--no-tests=error")
|
|
151
|
+
if configuration is not None:
|
|
152
|
+
arguments.extend(("--build-config", configuration))
|
|
153
|
+
if names is not None:
|
|
154
|
+
pattern = "^(" + "|".join(re.escape(name) for name in names) + ")$"
|
|
155
|
+
arguments.extend(("--tests-regex", pattern))
|
|
156
|
+
if parallel is not None:
|
|
157
|
+
arguments.extend(("--parallel", str(parallel)))
|
|
158
|
+
buffers = {"stdout": "", "stderr": ""}
|
|
159
|
+
|
|
160
|
+
async def parse_progress(line: str) -> None:
|
|
161
|
+
if on_progress is not None and re.search(
|
|
162
|
+
r"Start\s+\d+:|\d+/\d+\s+Test|tests passed|Total Test time",
|
|
163
|
+
line,
|
|
164
|
+
):
|
|
165
|
+
await on_progress(line.strip())
|
|
166
|
+
|
|
167
|
+
session = None
|
|
168
|
+
try:
|
|
169
|
+
async with await processes.launch(
|
|
170
|
+
path,
|
|
171
|
+
arguments,
|
|
172
|
+
cwd=source,
|
|
173
|
+
env=environment,
|
|
174
|
+
inherit_environment=inherit_environment,
|
|
175
|
+
timeout=timeout,
|
|
176
|
+
) as session:
|
|
177
|
+
await session.close_stdin()
|
|
178
|
+
async for chunk in session.output():
|
|
179
|
+
buffers[chunk.stream] += chunk.text.replace("\r", "\n")
|
|
180
|
+
while "\n" in buffers[chunk.stream]:
|
|
181
|
+
line, _, buffers[chunk.stream] = buffers[chunk.stream].partition("\n")
|
|
182
|
+
await parse_progress(line)
|
|
183
|
+
for line in buffers.values():
|
|
184
|
+
await parse_progress(line)
|
|
185
|
+
code = await session.wait()
|
|
186
|
+
cases = parse_report(report_path) if report_path.exists() or code == 0 else []
|
|
187
|
+
return TestResult(
|
|
188
|
+
return_code=code,
|
|
189
|
+
process_id=session.process_id,
|
|
190
|
+
tests=cases,
|
|
191
|
+
)
|
|
192
|
+
except (ProcessError, ToolParserError) as error:
|
|
193
|
+
raise ToolCommandError(
|
|
194
|
+
str(error),
|
|
195
|
+
process_id=session.process_id if session is not None else None,
|
|
196
|
+
) from error
|
|
197
|
+
|
|
198
|
+
methods: Methods = {"version": version, "test": test}
|
|
199
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
INFO = ToolInfo(name='ctest', kind=ToolKind.TEST_RUNNER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for gcc."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('-dumpfullversion', '-dumpversion'),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'^([0-9]+\\.[0-9]+[^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='gcc', kind=ToolKind.COMPILER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for gdb."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('--version',),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'GNU gdb(?: \\([^\\r\\n]*\\))? ([0-9]+\\.[0-9]+[^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='gdb', kind=ToolKind.DEBUGGER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for git."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('--version',),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'git version ([0-9][^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='git', kind=ToolKind.VERSION_CONTROL, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for g++."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('-dumpfullversion', '-dumpversion'),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'^([0-9]+\\.[0-9]+[^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='g++', kind=ToolKind.COMPILER, create_spec=create_spec)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Typed methods for link."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from forgemcp.process.service import ProcessService
|
|
8
|
+
|
|
9
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Methods(TypedDict):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_spec(
|
|
17
|
+
path: Path,
|
|
18
|
+
processes: ProcessService,
|
|
19
|
+
environment: Mapping[str, str] | None = None,
|
|
20
|
+
inherit_environment: bool = True,
|
|
21
|
+
) -> ToolSpec:
|
|
22
|
+
path = path.resolve()
|
|
23
|
+
methods: Methods = {}
|
|
24
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
INFO = ToolInfo(name='link', kind=ToolKind.LINKER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for lld."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('-flavor', 'gnu', '--version'),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'LLD ([0-9][^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='lld', kind=ToolKind.LINKER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for lld-link."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('--version',),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'LLD ([0-9][^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='lld-link', kind=ToolKind.LINKER, create_spec=create_spec)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Typed methods for lldb-dap."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from forgemcp.process.service import ProcessService
|
|
8
|
+
|
|
9
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Methods(TypedDict):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_spec(
|
|
17
|
+
path: Path,
|
|
18
|
+
processes: ProcessService,
|
|
19
|
+
environment: Mapping[str, str] | None = None,
|
|
20
|
+
inherit_environment: bool = True,
|
|
21
|
+
) -> ToolSpec:
|
|
22
|
+
path = path.resolve()
|
|
23
|
+
methods: Methods = {}
|
|
24
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
INFO = ToolInfo(name='lldb-dap', kind=ToolKind.DEBUGGER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for make."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('--version',),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'GNU Make ([0-9][^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='make', kind=ToolKind.BUILD_RUNNER, create_spec=create_spec)
|