eth-mcp 0.2.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.
- eth_mcp-0.2.0.dist-info/METADATA +332 -0
- eth_mcp-0.2.0.dist-info/RECORD +21 -0
- eth_mcp-0.2.0.dist-info/WHEEL +4 -0
- eth_mcp-0.2.0.dist-info/entry_points.txt +3 -0
- ethereum_mcp/__init__.py +3 -0
- ethereum_mcp/cli.py +589 -0
- ethereum_mcp/clients.py +363 -0
- ethereum_mcp/config.py +324 -0
- ethereum_mcp/expert/__init__.py +1 -0
- ethereum_mcp/expert/guidance.py +300 -0
- ethereum_mcp/indexer/__init__.py +8 -0
- ethereum_mcp/indexer/chunker.py +563 -0
- ethereum_mcp/indexer/client_compiler.py +725 -0
- ethereum_mcp/indexer/compiler.py +245 -0
- ethereum_mcp/indexer/downloader.py +521 -0
- ethereum_mcp/indexer/embedder.py +627 -0
- ethereum_mcp/indexer/manifest.py +411 -0
- ethereum_mcp/logging.py +85 -0
- ethereum_mcp/models.py +126 -0
- ethereum_mcp/server.py +555 -0
- ethereum_mcp/tools/__init__.py +1 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Download Ethereum specs, EIPs, client source code, and MEV infrastructure from GitHub.
|
|
2
|
+
|
|
3
|
+
Repositories:
|
|
4
|
+
- ethereum/consensus-specs: Consensus layer specifications
|
|
5
|
+
- ethereum/EIPs: Ethereum Improvement Proposals
|
|
6
|
+
|
|
7
|
+
Execution Layer Clients:
|
|
8
|
+
- paradigmxyz/reth: Rust execution client (Paradigm)
|
|
9
|
+
- ethereum/go-ethereum: Go execution client (Geth)
|
|
10
|
+
- NethermindEth/nethermind: C# execution client
|
|
11
|
+
- ledgerwatch/erigon: Go execution client (archive-focused)
|
|
12
|
+
|
|
13
|
+
Consensus Layer Clients:
|
|
14
|
+
- sigp/lighthouse: Rust consensus client (Sigma Prime)
|
|
15
|
+
- prysmaticlabs/prysm: Go consensus client
|
|
16
|
+
- ConsenSys/teku: Java consensus client
|
|
17
|
+
- status-im/nimbus-eth2: Nim consensus client
|
|
18
|
+
|
|
19
|
+
MEV Infrastructure (Flashbots):
|
|
20
|
+
- flashbots/mev-boost: MEV-boost middleware (connects validators to builders)
|
|
21
|
+
- flashbots/builder: Block builder reference implementation
|
|
22
|
+
- flashbots/mev-boost-relay: Relay implementation
|
|
23
|
+
- flashbots/flashbots-protect-rpc: RPC for private transactions
|
|
24
|
+
- ethereum/builder-specs: Builder API specifications
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from git import Repo
|
|
34
|
+
|
|
35
|
+
from ..logging import get_logger
|
|
36
|
+
|
|
37
|
+
logger = get_logger("downloader")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class SpecsConfig:
|
|
42
|
+
"""Configuration for specs download."""
|
|
43
|
+
|
|
44
|
+
consensus_specs_url: str = "https://github.com/ethereum/consensus-specs.git"
|
|
45
|
+
eips_url: str = "https://github.com/ethereum/EIPs.git"
|
|
46
|
+
builder_specs_url: str = "https://github.com/ethereum/builder-specs.git"
|
|
47
|
+
consensus_specs_branch: str = "master"
|
|
48
|
+
eips_branch: str = "master"
|
|
49
|
+
builder_specs_branch: str = "main"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Client repositories to index
|
|
53
|
+
# Using sparse checkout for large repos to only get relevant code
|
|
54
|
+
CLIENT_REPOS = {
|
|
55
|
+
# Execution Layer Clients
|
|
56
|
+
"reth": {
|
|
57
|
+
"url": "https://github.com/paradigmxyz/reth.git",
|
|
58
|
+
"branch": "main",
|
|
59
|
+
"language": "rust",
|
|
60
|
+
"layer": "execution",
|
|
61
|
+
"sparse_paths": [
|
|
62
|
+
"crates/consensus",
|
|
63
|
+
"crates/engine",
|
|
64
|
+
"crates/evm",
|
|
65
|
+
"crates/execution",
|
|
66
|
+
"crates/net",
|
|
67
|
+
"crates/payload",
|
|
68
|
+
"crates/primitives",
|
|
69
|
+
"crates/rpc",
|
|
70
|
+
"crates/stages",
|
|
71
|
+
"crates/storage",
|
|
72
|
+
"crates/transaction-pool",
|
|
73
|
+
"crates/trie",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
"go-ethereum": {
|
|
77
|
+
"url": "https://github.com/ethereum/go-ethereum.git",
|
|
78
|
+
"branch": "master",
|
|
79
|
+
"language": "go",
|
|
80
|
+
"layer": "execution",
|
|
81
|
+
"sparse_paths": [
|
|
82
|
+
"consensus",
|
|
83
|
+
"core",
|
|
84
|
+
"eth",
|
|
85
|
+
"ethdb",
|
|
86
|
+
"miner",
|
|
87
|
+
"node",
|
|
88
|
+
"p2p",
|
|
89
|
+
"params",
|
|
90
|
+
"rpc",
|
|
91
|
+
"trie",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
"nethermind": {
|
|
95
|
+
"url": "https://github.com/NethermindEth/nethermind.git",
|
|
96
|
+
"branch": "master",
|
|
97
|
+
"language": "csharp",
|
|
98
|
+
"layer": "execution",
|
|
99
|
+
"sparse_paths": [
|
|
100
|
+
"src/Nethermind/Nethermind.Consensus",
|
|
101
|
+
"src/Nethermind/Nethermind.Core",
|
|
102
|
+
"src/Nethermind/Nethermind.Evm",
|
|
103
|
+
"src/Nethermind/Nethermind.JsonRpc",
|
|
104
|
+
"src/Nethermind/Nethermind.Merge.Plugin",
|
|
105
|
+
"src/Nethermind/Nethermind.Network",
|
|
106
|
+
"src/Nethermind/Nethermind.State",
|
|
107
|
+
"src/Nethermind/Nethermind.Trie",
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
"erigon": {
|
|
111
|
+
"url": "https://github.com/ledgerwatch/erigon.git",
|
|
112
|
+
"branch": "main",
|
|
113
|
+
"language": "go",
|
|
114
|
+
"layer": "execution",
|
|
115
|
+
"sparse_paths": [
|
|
116
|
+
"consensus",
|
|
117
|
+
"core",
|
|
118
|
+
"erigon-lib",
|
|
119
|
+
"eth",
|
|
120
|
+
"ethdb",
|
|
121
|
+
"turbo",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
# Consensus Layer Clients
|
|
125
|
+
"lighthouse": {
|
|
126
|
+
"url": "https://github.com/sigp/lighthouse.git",
|
|
127
|
+
"branch": "stable",
|
|
128
|
+
"language": "rust",
|
|
129
|
+
"layer": "consensus",
|
|
130
|
+
"sparse_paths": [
|
|
131
|
+
"beacon_node/beacon_chain",
|
|
132
|
+
"beacon_node/client",
|
|
133
|
+
"beacon_node/execution_layer",
|
|
134
|
+
"beacon_node/lighthouse_network",
|
|
135
|
+
"beacon_node/network",
|
|
136
|
+
"beacon_node/store",
|
|
137
|
+
"consensus/fork_choice",
|
|
138
|
+
"consensus/state_processing",
|
|
139
|
+
"consensus/types",
|
|
140
|
+
"slasher",
|
|
141
|
+
"validator_client",
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
"prysm": {
|
|
145
|
+
"url": "https://github.com/prysmaticlabs/prysm.git",
|
|
146
|
+
"branch": "develop",
|
|
147
|
+
"language": "go",
|
|
148
|
+
"layer": "consensus",
|
|
149
|
+
"sparse_paths": [
|
|
150
|
+
"beacon-chain/blockchain",
|
|
151
|
+
"beacon-chain/core",
|
|
152
|
+
"beacon-chain/db",
|
|
153
|
+
"beacon-chain/execution",
|
|
154
|
+
"beacon-chain/forkchoice",
|
|
155
|
+
"beacon-chain/operations",
|
|
156
|
+
"beacon-chain/p2p",
|
|
157
|
+
"beacon-chain/slasher",
|
|
158
|
+
"beacon-chain/state",
|
|
159
|
+
"beacon-chain/sync",
|
|
160
|
+
"proto",
|
|
161
|
+
"validator",
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
"teku": {
|
|
165
|
+
"url": "https://github.com/ConsenSys/teku.git",
|
|
166
|
+
"branch": "master",
|
|
167
|
+
"language": "java",
|
|
168
|
+
"layer": "consensus",
|
|
169
|
+
"sparse_paths": [
|
|
170
|
+
"ethereum/spec",
|
|
171
|
+
"ethereum/statetransition",
|
|
172
|
+
"ethereum/executionclient",
|
|
173
|
+
"ethereum/executionlayer",
|
|
174
|
+
"ethereum/networks",
|
|
175
|
+
"ethereum/pow",
|
|
176
|
+
"networking",
|
|
177
|
+
"storage",
|
|
178
|
+
"validator",
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
"nimbus-eth2": {
|
|
182
|
+
"url": "https://github.com/status-im/nimbus-eth2.git",
|
|
183
|
+
"branch": "stable",
|
|
184
|
+
"language": "nim",
|
|
185
|
+
"layer": "consensus",
|
|
186
|
+
"sparse_paths": [
|
|
187
|
+
"beacon_chain",
|
|
188
|
+
"ncli",
|
|
189
|
+
"research",
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
# ===================
|
|
193
|
+
# MEV Infrastructure
|
|
194
|
+
# ===================
|
|
195
|
+
"mev-boost": {
|
|
196
|
+
"url": "https://github.com/flashbots/mev-boost.git",
|
|
197
|
+
"branch": "develop",
|
|
198
|
+
"language": "go",
|
|
199
|
+
"layer": "mev",
|
|
200
|
+
"sparse_paths": None, # Small repo, full clone
|
|
201
|
+
"description": "MEV-boost middleware connecting validators to block builders",
|
|
202
|
+
},
|
|
203
|
+
"flashbots-builder": {
|
|
204
|
+
"url": "https://github.com/flashbots/builder.git",
|
|
205
|
+
"branch": "main",
|
|
206
|
+
"language": "go",
|
|
207
|
+
"layer": "mev",
|
|
208
|
+
"sparse_paths": [
|
|
209
|
+
"builder",
|
|
210
|
+
"core",
|
|
211
|
+
"eth",
|
|
212
|
+
"miner",
|
|
213
|
+
"flashbotsextra",
|
|
214
|
+
],
|
|
215
|
+
"description": "Flashbots block builder (geth fork)",
|
|
216
|
+
},
|
|
217
|
+
"mev-boost-relay": {
|
|
218
|
+
"url": "https://github.com/flashbots/mev-boost-relay.git",
|
|
219
|
+
"branch": "main",
|
|
220
|
+
"language": "go",
|
|
221
|
+
"layer": "mev",
|
|
222
|
+
"sparse_paths": None, # Full clone
|
|
223
|
+
"description": "MEV-boost relay for connecting builders to proposers",
|
|
224
|
+
},
|
|
225
|
+
"builder-specs": {
|
|
226
|
+
"url": "https://github.com/ethereum/builder-specs.git",
|
|
227
|
+
"branch": "main",
|
|
228
|
+
"language": "markdown",
|
|
229
|
+
"layer": "mev",
|
|
230
|
+
"sparse_paths": None, # Specs repo, full clone
|
|
231
|
+
"description": "Builder API specifications for PBS",
|
|
232
|
+
},
|
|
233
|
+
"mev-share-node": {
|
|
234
|
+
"url": "https://github.com/flashbots/mev-share-node.git",
|
|
235
|
+
"branch": "main",
|
|
236
|
+
"language": "go",
|
|
237
|
+
"layer": "mev",
|
|
238
|
+
"sparse_paths": None,
|
|
239
|
+
"description": "MEV-Share node for orderflow auctions",
|
|
240
|
+
},
|
|
241
|
+
"rbuilder": {
|
|
242
|
+
"url": "https://github.com/flashbots/rbuilder.git",
|
|
243
|
+
"branch": "develop",
|
|
244
|
+
"language": "rust",
|
|
245
|
+
"layer": "mev",
|
|
246
|
+
"sparse_paths": [
|
|
247
|
+
"crates/rbuilder",
|
|
248
|
+
"crates/op-rbuilder",
|
|
249
|
+
],
|
|
250
|
+
"description": "Rust block builder by Flashbots (high performance)",
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def run_git(args: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess:
|
|
256
|
+
"""Run a git command."""
|
|
257
|
+
result = subprocess.run(
|
|
258
|
+
["git"] + args,
|
|
259
|
+
cwd=cwd,
|
|
260
|
+
capture_output=True,
|
|
261
|
+
text=True,
|
|
262
|
+
)
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def clone_client_repo(
|
|
267
|
+
name: str,
|
|
268
|
+
url: str,
|
|
269
|
+
dest: Path,
|
|
270
|
+
branch: str = "main",
|
|
271
|
+
sparse_paths: list[str] | None = None,
|
|
272
|
+
progress_callback: Callable[[str], None] | None = None,
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""
|
|
275
|
+
Clone a client repository with sparse checkout.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
name: Repository name for logging
|
|
279
|
+
url: Git URL to clone
|
|
280
|
+
dest: Destination path
|
|
281
|
+
branch: Branch to checkout
|
|
282
|
+
sparse_paths: Paths to checkout (sparse checkout)
|
|
283
|
+
progress_callback: Optional callback for progress updates
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if successful, False otherwise
|
|
287
|
+
"""
|
|
288
|
+
def log(msg: str):
|
|
289
|
+
if progress_callback:
|
|
290
|
+
progress_callback(msg)
|
|
291
|
+
else:
|
|
292
|
+
logger.info(msg)
|
|
293
|
+
|
|
294
|
+
if dest.exists():
|
|
295
|
+
log(f" {name}: Already exists, pulling latest...")
|
|
296
|
+
result = run_git(["pull", "--ff-only"], cwd=dest)
|
|
297
|
+
if result.returncode != 0:
|
|
298
|
+
log(f" {name}: Pull failed, trying fetch + reset...")
|
|
299
|
+
run_git(["fetch", "origin"], cwd=dest)
|
|
300
|
+
run_git(["reset", "--hard", f"origin/{branch}"], cwd=dest)
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
log(f" {name}: Cloning from {url}...")
|
|
304
|
+
|
|
305
|
+
if sparse_paths:
|
|
306
|
+
# Sparse checkout for large repos
|
|
307
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
|
|
309
|
+
# Initialize repo
|
|
310
|
+
run_git(["init"], cwd=dest)
|
|
311
|
+
run_git(["remote", "add", "origin", url], cwd=dest)
|
|
312
|
+
|
|
313
|
+
# Configure sparse checkout
|
|
314
|
+
run_git(["config", "core.sparseCheckout", "true"], cwd=dest)
|
|
315
|
+
|
|
316
|
+
# Write sparse-checkout file
|
|
317
|
+
sparse_file = dest / ".git" / "info" / "sparse-checkout"
|
|
318
|
+
sparse_file.parent.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
sparse_file.write_text("\n".join(sparse_paths) + "\n")
|
|
320
|
+
|
|
321
|
+
# Fetch and checkout
|
|
322
|
+
log(f" {name}: Fetching (sparse checkout: {len(sparse_paths)} paths)...")
|
|
323
|
+
result = run_git(["fetch", "--depth=1", "origin", branch], cwd=dest)
|
|
324
|
+
if result.returncode != 0:
|
|
325
|
+
log(f" {name}: Fetch failed: {result.stderr}")
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
result = run_git(["checkout", branch], cwd=dest)
|
|
329
|
+
if result.returncode != 0:
|
|
330
|
+
log(f" {name}: Checkout failed: {result.stderr}")
|
|
331
|
+
return False
|
|
332
|
+
else:
|
|
333
|
+
# Full clone for small repos
|
|
334
|
+
result = run_git(["clone", "--depth=1", "--branch", branch, url, str(dest)])
|
|
335
|
+
if result.returncode != 0:
|
|
336
|
+
log(f" {name}: Clone failed: {result.stderr}")
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
log(f" {name}: Done")
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def download_specs(
|
|
344
|
+
data_dir: Path,
|
|
345
|
+
config: SpecsConfig | None = None,
|
|
346
|
+
force: bool = False,
|
|
347
|
+
) -> tuple[Path, Path]:
|
|
348
|
+
"""
|
|
349
|
+
Download consensus specs and EIPs repositories.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
data_dir: Directory to store downloaded repos
|
|
353
|
+
config: Optional configuration override
|
|
354
|
+
force: If True, re-download even if exists
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Tuple of (consensus_specs_path, eips_path)
|
|
358
|
+
"""
|
|
359
|
+
config = config or SpecsConfig()
|
|
360
|
+
data_dir = Path(data_dir)
|
|
361
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
362
|
+
|
|
363
|
+
consensus_dir = data_dir / "consensus-specs"
|
|
364
|
+
eips_dir = data_dir / "EIPs"
|
|
365
|
+
|
|
366
|
+
# Download consensus specs
|
|
367
|
+
if force and consensus_dir.exists():
|
|
368
|
+
shutil.rmtree(consensus_dir)
|
|
369
|
+
|
|
370
|
+
if not consensus_dir.exists():
|
|
371
|
+
logger.info("Cloning consensus-specs to %s...", consensus_dir)
|
|
372
|
+
Repo.clone_from(
|
|
373
|
+
config.consensus_specs_url,
|
|
374
|
+
consensus_dir,
|
|
375
|
+
branch=config.consensus_specs_branch,
|
|
376
|
+
depth=1,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
logger.info("Consensus specs already exists at %s, pulling latest...", consensus_dir)
|
|
380
|
+
repo = Repo(consensus_dir)
|
|
381
|
+
repo.remotes.origin.pull()
|
|
382
|
+
|
|
383
|
+
# Download EIPs
|
|
384
|
+
if force and eips_dir.exists():
|
|
385
|
+
shutil.rmtree(eips_dir)
|
|
386
|
+
|
|
387
|
+
if not eips_dir.exists():
|
|
388
|
+
logger.info("Cloning EIPs to %s...", eips_dir)
|
|
389
|
+
Repo.clone_from(
|
|
390
|
+
config.eips_url,
|
|
391
|
+
eips_dir,
|
|
392
|
+
branch=config.eips_branch,
|
|
393
|
+
depth=1,
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
logger.info("EIPs already exists at %s, pulling latest...", eips_dir)
|
|
397
|
+
repo = Repo(eips_dir)
|
|
398
|
+
repo.remotes.origin.pull()
|
|
399
|
+
|
|
400
|
+
# Download builder-specs
|
|
401
|
+
builder_specs_dir = data_dir / "builder-specs"
|
|
402
|
+
if force and builder_specs_dir.exists():
|
|
403
|
+
shutil.rmtree(builder_specs_dir)
|
|
404
|
+
|
|
405
|
+
if not builder_specs_dir.exists():
|
|
406
|
+
logger.info("Cloning builder-specs to %s...", builder_specs_dir)
|
|
407
|
+
Repo.clone_from(
|
|
408
|
+
config.builder_specs_url,
|
|
409
|
+
builder_specs_dir,
|
|
410
|
+
branch=config.builder_specs_branch,
|
|
411
|
+
depth=1,
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
logger.info("Builder specs already exists at %s, pulling latest...", builder_specs_dir)
|
|
415
|
+
repo = Repo(builder_specs_dir)
|
|
416
|
+
repo.remotes.origin.pull()
|
|
417
|
+
|
|
418
|
+
return consensus_dir, eips_dir, builder_specs_dir
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def download_clients(
|
|
422
|
+
data_dir: Path,
|
|
423
|
+
clients: list[str] | None = None,
|
|
424
|
+
progress_callback: Callable[[str], None] | None = None,
|
|
425
|
+
) -> dict[str, bool]:
|
|
426
|
+
"""
|
|
427
|
+
Download Ethereum client source code.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
data_dir: Directory to store downloaded repos
|
|
431
|
+
clients: List of client names to download (default: all)
|
|
432
|
+
progress_callback: Optional callback for progress updates
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Dict mapping client name to success status
|
|
436
|
+
"""
|
|
437
|
+
data_dir = Path(data_dir)
|
|
438
|
+
clients_dir = data_dir / "clients"
|
|
439
|
+
clients_dir.mkdir(parents=True, exist_ok=True)
|
|
440
|
+
|
|
441
|
+
if clients is None:
|
|
442
|
+
clients = list(CLIENT_REPOS.keys())
|
|
443
|
+
|
|
444
|
+
results = {}
|
|
445
|
+
for name in clients:
|
|
446
|
+
if name not in CLIENT_REPOS:
|
|
447
|
+
if progress_callback:
|
|
448
|
+
progress_callback(f" {name}: Unknown client, skipping")
|
|
449
|
+
results[name] = False
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
config = CLIENT_REPOS[name]
|
|
453
|
+
dest = clients_dir / name
|
|
454
|
+
|
|
455
|
+
success = clone_client_repo(
|
|
456
|
+
name=name,
|
|
457
|
+
url=config["url"],
|
|
458
|
+
dest=dest,
|
|
459
|
+
branch=config["branch"],
|
|
460
|
+
sparse_paths=config.get("sparse_paths"),
|
|
461
|
+
progress_callback=progress_callback,
|
|
462
|
+
)
|
|
463
|
+
results[name] = success
|
|
464
|
+
|
|
465
|
+
return results
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def list_downloaded_clients(data_dir: Path) -> dict[str, dict]:
|
|
469
|
+
"""List all downloaded client repositories with their status."""
|
|
470
|
+
clients_dir = data_dir / "clients"
|
|
471
|
+
|
|
472
|
+
status = {}
|
|
473
|
+
for name, config in CLIENT_REPOS.items():
|
|
474
|
+
path = clients_dir / name
|
|
475
|
+
if path.exists():
|
|
476
|
+
result = run_git(["rev-parse", "HEAD"], cwd=path)
|
|
477
|
+
version = result.stdout.strip()[:12] if result.returncode == 0 else None
|
|
478
|
+
status[name] = {
|
|
479
|
+
"path": str(path),
|
|
480
|
+
"version": version,
|
|
481
|
+
"exists": True,
|
|
482
|
+
"language": config["language"],
|
|
483
|
+
"layer": config["layer"],
|
|
484
|
+
}
|
|
485
|
+
else:
|
|
486
|
+
status[name] = {
|
|
487
|
+
"path": str(path),
|
|
488
|
+
"version": None,
|
|
489
|
+
"exists": False,
|
|
490
|
+
"language": config["language"],
|
|
491
|
+
"layer": config["layer"],
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return status
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def get_spec_files(consensus_dir: Path) -> list[Path]:
|
|
498
|
+
"""Get all markdown spec files organized by fork."""
|
|
499
|
+
specs_dir = consensus_dir / "specs"
|
|
500
|
+
if not specs_dir.exists():
|
|
501
|
+
raise FileNotFoundError(f"Specs directory not found: {specs_dir}")
|
|
502
|
+
|
|
503
|
+
return list(specs_dir.rglob("*.md"))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def get_eip_files(eips_dir: Path) -> list[Path]:
|
|
507
|
+
"""Get all EIP markdown files."""
|
|
508
|
+
eips_content_dir = eips_dir / "EIPS"
|
|
509
|
+
if not eips_content_dir.exists():
|
|
510
|
+
raise FileNotFoundError(f"EIPs directory not found: {eips_content_dir}")
|
|
511
|
+
|
|
512
|
+
return list(eips_content_dir.glob("eip-*.md"))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def get_builder_spec_files(builder_specs_dir: Path) -> list[Path]:
|
|
516
|
+
"""Get all builder-specs markdown files."""
|
|
517
|
+
specs_dir = builder_specs_dir / "specs"
|
|
518
|
+
if not specs_dir.exists():
|
|
519
|
+
raise FileNotFoundError(f"Builder specs directory not found: {specs_dir}")
|
|
520
|
+
|
|
521
|
+
return list(specs_dir.rglob("*.md"))
|