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.
Files changed (83) hide show
  1. forge_cpp_mcp-0.2.1.dist-info/METADATA +436 -0
  2. forge_cpp_mcp-0.2.1.dist-info/RECORD +83 -0
  3. forge_cpp_mcp-0.2.1.dist-info/WHEEL +4 -0
  4. forge_cpp_mcp-0.2.1.dist-info/entry_points.txt +2 -0
  5. forge_cpp_mcp-0.2.1.dist-info/licenses/LICENSE +21 -0
  6. forgemcp/__init__.py +5 -0
  7. forgemcp/assets/clangd-result.html +119 -0
  8. forgemcp/assets/cmake-build.html +111 -0
  9. forgemcp/assets/cmake-configure.html +111 -0
  10. forgemcp/assets/cmake-profiles.html +111 -0
  11. forgemcp/assets/cmake-test.html +111 -0
  12. forgemcp/assets/process-details.html +113 -0
  13. forgemcp/assets/process-overview.html +111 -0
  14. forgemcp/assets/toolsets.html +111 -0
  15. forgemcp/assets/workspace-file.html +119 -0
  16. forgemcp/assets/workspace-result.html +119 -0
  17. forgemcp/assets/workspace-search.html +119 -0
  18. forgemcp/assets/workspace-tree.html +119 -0
  19. forgemcp/assets.py +62 -0
  20. forgemcp/clangd/__init__.py +1 -0
  21. forgemcp/clangd/errors.py +29 -0
  22. forgemcp/clangd/models.py +94 -0
  23. forgemcp/clangd/service.py +1279 -0
  24. forgemcp/clangd/session.py +520 -0
  25. forgemcp/cmake/__init__.py +1 -0
  26. forgemcp/cmake/errors.py +5 -0
  27. forgemcp/cmake/profiles.py +95 -0
  28. forgemcp/cmake/service.py +895 -0
  29. forgemcp/completion.py +49 -0
  30. forgemcp/icons/clangd.LICENSE +219 -0
  31. forgemcp/icons/clangd.svg +25 -0
  32. forgemcp/icons/cmake.svg +9 -0
  33. forgemcp/icons/process.svg +4 -0
  34. forgemcp/icons/toolchain.svg +1 -0
  35. forgemcp/icons/workspace-edit.svg +1 -0
  36. forgemcp/icons/workspace-file.svg +1 -0
  37. forgemcp/icons/workspace-search.svg +1 -0
  38. forgemcp/icons/workspace.svg +4 -0
  39. forgemcp/markdown.py +308 -0
  40. forgemcp/process/__init__.py +21 -0
  41. forgemcp/process/errors.py +21 -0
  42. forgemcp/process/models.py +171 -0
  43. forgemcp/process/service.py +830 -0
  44. forgemcp/process/transcript.py +116 -0
  45. forgemcp/progress.py +40 -0
  46. forgemcp/server.py +196 -0
  47. forgemcp/toolchain/__init__.py +1 -0
  48. forgemcp/toolchain/discovery.py +25 -0
  49. forgemcp/toolchain/errors.py +41 -0
  50. forgemcp/toolchain/loader.py +48 -0
  51. forgemcp/toolchain/providers/__init__.py +1 -0
  52. forgemcp/toolchain/providers/system.py +52 -0
  53. forgemcp/toolchain/providers/user.py +93 -0
  54. forgemcp/toolchain/providers/visual_studio.py +202 -0
  55. forgemcp/toolchain/service.py +226 -0
  56. forgemcp/toolchain/spec.py +73 -0
  57. forgemcp/toolchain/tools/__init__.py +1 -0
  58. forgemcp/toolchain/tools/clang.py +61 -0
  59. forgemcp/toolchain/tools/clang_cl.py +61 -0
  60. forgemcp/toolchain/tools/clangd.py +280 -0
  61. forgemcp/toolchain/tools/clangxx.py +61 -0
  62. forgemcp/toolchain/tools/cmake.py +280 -0
  63. forgemcp/toolchain/tools/cppvsdbg.py +27 -0
  64. forgemcp/toolchain/tools/ctest.py +202 -0
  65. forgemcp/toolchain/tools/gcc.py +61 -0
  66. forgemcp/toolchain/tools/gdb.py +61 -0
  67. forgemcp/toolchain/tools/git.py +61 -0
  68. forgemcp/toolchain/tools/gxx.py +61 -0
  69. forgemcp/toolchain/tools/link.py +27 -0
  70. forgemcp/toolchain/tools/lld.py +61 -0
  71. forgemcp/toolchain/tools/lld_link.py +61 -0
  72. forgemcp/toolchain/tools/lldb_dap.py +27 -0
  73. forgemcp/toolchain/tools/make.py +61 -0
  74. forgemcp/toolchain/tools/msbuild.py +61 -0
  75. forgemcp/toolchain/tools/msvc.py +27 -0
  76. forgemcp/toolchain/tools/ninja.py +61 -0
  77. forgemcp/workspace/__init__.py +5 -0
  78. forgemcp/workspace/diff.py +181 -0
  79. forgemcp/workspace/errors.py +5 -0
  80. forgemcp/workspace/metadata.py +87 -0
  81. forgemcp/workspace/path.py +71 -0
  82. forgemcp/workspace/providers.py +25 -0
  83. 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)