codetool-explore 0.5.0__py3-none-win_arm64.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.
- codetool_explore/__init__.py +35 -0
- codetool_explore/_bin/codetool-explore-rust-windows-arm64.exe +0 -0
- codetool_explore/api.py +266 -0
- codetool_explore/cli.py +188 -0
- codetool_explore/compression.py +150 -0
- codetool_explore/cursor.py +71 -0
- codetool_explore/errors.py +23 -0
- codetool_explore/explorer.py +497 -0
- codetool_explore/ignore.py +222 -0
- codetool_explore/py.typed +0 -0
- codetool_explore/python_backend/__init__.py +154 -0
- codetool_explore/python_backend/case.py +19 -0
- codetool_explore/python_backend/config.py +35 -0
- codetool_explore/python_backend/constants.py +39 -0
- codetool_explore/python_backend/file_search.py +51 -0
- codetool_explore/python_backend/ignore_rules.py +40 -0
- codetool_explore/python_backend/literal.py +79 -0
- codetool_explore/python_backend/matcher.py +79 -0
- codetool_explore/python_backend/models.py +49 -0
- codetool_explore/python_backend/output.py +82 -0
- codetool_explore/python_backend/regex_search.py +63 -0
- codetool_explore/python_backend/search.py +327 -0
- codetool_explore/python_backend/text.py +39 -0
- codetool_explore/python_backend/walker.py +119 -0
- codetool_explore/ranking.py +384 -0
- codetool_explore/roots.py +148 -0
- codetool_explore/rust_backend.py +308 -0
- codetool_explore/text_output.py +475 -0
- codetool_explore-0.5.0.dist-info/METADATA +240 -0
- codetool_explore-0.5.0.dist-info/RECORD +33 -0
- codetool_explore-0.5.0.dist-info/WHEEL +4 -0
- codetool_explore-0.5.0.dist-info/entry_points.txt +2 -0
- codetool_explore-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Rust CLI backend discovery and subprocess wrapper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .cursor import normalize_limit
|
|
15
|
+
from .errors import (
|
|
16
|
+
ExploreArgumentError,
|
|
17
|
+
ExploreBackendError,
|
|
18
|
+
ExplorePatternError,
|
|
19
|
+
)
|
|
20
|
+
from .ignore import normalize_patterns
|
|
21
|
+
from .python_backend import normalize_mode, normalize_path_scope, normalize_target, resolve_case
|
|
22
|
+
from .roots import RootInput, normalize_search_roots
|
|
23
|
+
|
|
24
|
+
ENV_BINARY = "CODETOOL_EXPLORE_RUST_BINARY"
|
|
25
|
+
TARGET_RUNTIME_ENVS = (
|
|
26
|
+
"CODETOOL_EXPLORE_TARGET_RUNTIME",
|
|
27
|
+
"CODETOOL_TARGET_RUNTIME",
|
|
28
|
+
)
|
|
29
|
+
SUPPORTED_RUNTIME_KEYS = (
|
|
30
|
+
"linux-x86_64",
|
|
31
|
+
"linux-aarch64",
|
|
32
|
+
"macos-x86_64",
|
|
33
|
+
"macos-arm64",
|
|
34
|
+
"windows-x86_64",
|
|
35
|
+
"windows-arm64",
|
|
36
|
+
)
|
|
37
|
+
RUNTIME_ALIASES = {
|
|
38
|
+
"linux-amd64": "linux-x86_64",
|
|
39
|
+
"linux-x64": "linux-x86_64",
|
|
40
|
+
"linux-arm64": "linux-aarch64",
|
|
41
|
+
"darwin-x86_64": "macos-x86_64",
|
|
42
|
+
"darwin-amd64": "macos-x86_64",
|
|
43
|
+
"darwin-arm64": "macos-arm64",
|
|
44
|
+
"darwin-aarch64": "macos-arm64",
|
|
45
|
+
"macos-aarch64": "macos-arm64",
|
|
46
|
+
"macosx-x86_64": "macos-x86_64",
|
|
47
|
+
"macosx-arm64": "macos-arm64",
|
|
48
|
+
"windows-amd64": "windows-x86_64",
|
|
49
|
+
"windows-x64": "windows-x86_64",
|
|
50
|
+
"windows-aarch64": "windows-arm64",
|
|
51
|
+
"win-x86_64": "windows-x86_64",
|
|
52
|
+
"win-amd64": "windows-x86_64",
|
|
53
|
+
"win-arm64": "windows-arm64",
|
|
54
|
+
}
|
|
55
|
+
BINARY_NAMES: tuple[str, ...] = (
|
|
56
|
+
"codetool-explore-rust",
|
|
57
|
+
"codetool_explore_rust",
|
|
58
|
+
)
|
|
59
|
+
BASE_BINARY = "codetool-explore-rust"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RustBackendUnavailable(ExploreBackendError):
|
|
63
|
+
"""Raised when an explicit Rust backend request cannot be satisfied."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalize_machine(machine: str | None = None) -> str:
|
|
67
|
+
raw = (machine or platform.machine() or "unknown").lower().replace("-", "_")
|
|
68
|
+
aliases = {
|
|
69
|
+
"amd64": "x86_64",
|
|
70
|
+
"x64": "x86_64",
|
|
71
|
+
"i386": "x86",
|
|
72
|
+
"i686": "x86",
|
|
73
|
+
"arm64": "arm64",
|
|
74
|
+
"aarch64": "aarch64",
|
|
75
|
+
}
|
|
76
|
+
return aliases.get(raw, raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def normalize_runtime_key(value: str) -> str:
|
|
80
|
+
"""Normalize user/platform spelling to a stable packaged-binary runtime key."""
|
|
81
|
+
|
|
82
|
+
key = value.strip().lower().replace("_", "-")
|
|
83
|
+
key = key.replace("linux-x86-64", "linux-x86_64")
|
|
84
|
+
key = key.replace("macos-x86-64", "macos-x86_64")
|
|
85
|
+
key = key.replace("windows-x86-64", "windows-x86_64")
|
|
86
|
+
key = key.replace("darwin-x86-64", "darwin-x86_64")
|
|
87
|
+
key = RUNTIME_ALIASES.get(key, key)
|
|
88
|
+
if key not in SUPPORTED_RUNTIME_KEYS:
|
|
89
|
+
supported = ", ".join(SUPPORTED_RUNTIME_KEYS)
|
|
90
|
+
raise ExploreArgumentError(
|
|
91
|
+
f"unsupported target runtime {value!r}; expected one of: {supported}"
|
|
92
|
+
)
|
|
93
|
+
return key
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _host_runtime_key() -> str:
|
|
97
|
+
if os.name == "nt":
|
|
98
|
+
os_tag = "windows"
|
|
99
|
+
elif sys.platform == "darwin":
|
|
100
|
+
os_tag = "macos"
|
|
101
|
+
elif sys.platform.startswith("linux"):
|
|
102
|
+
os_tag = "linux"
|
|
103
|
+
else:
|
|
104
|
+
os_tag = sys.platform.replace("-", "_")
|
|
105
|
+
|
|
106
|
+
arch = _normalize_machine()
|
|
107
|
+
if os_tag == "linux" and arch == "arm64":
|
|
108
|
+
arch = "aarch64"
|
|
109
|
+
elif os_tag in {"macos", "windows"} and arch == "aarch64":
|
|
110
|
+
arch = "arm64"
|
|
111
|
+
return normalize_runtime_key(f"{os_tag}-{arch}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _target_runtime_key() -> str:
|
|
115
|
+
for env_name in TARGET_RUNTIME_ENVS:
|
|
116
|
+
if value := os.environ.get(env_name):
|
|
117
|
+
return normalize_runtime_key(value)
|
|
118
|
+
return _host_runtime_key()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _platform_tag() -> str:
|
|
122
|
+
"""Return the stable runtime key used for packaged Rust helper binaries."""
|
|
123
|
+
|
|
124
|
+
return _target_runtime_key()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def packaged_binary_names(runtime_key: str | None = None) -> tuple[str, ...]:
|
|
128
|
+
"""Return target-specific packaged binary names and generic binary names."""
|
|
129
|
+
|
|
130
|
+
tag = normalize_runtime_key(runtime_key) if runtime_key else _platform_tag()
|
|
131
|
+
names = [f"{BASE_BINARY}-{tag}"]
|
|
132
|
+
if tag == "linux-x86_64":
|
|
133
|
+
names.append(f"{BASE_BINARY}-linux-x86_64")
|
|
134
|
+
if tag.endswith("arm64"):
|
|
135
|
+
names.append(f"{BASE_BINARY}-{tag.replace('arm64', 'aarch64')}")
|
|
136
|
+
names.extend(BINARY_NAMES)
|
|
137
|
+
return tuple(dict.fromkeys(names))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _with_platform_suffix(name: str) -> tuple[str, ...]:
|
|
141
|
+
if os.name == "nt" and not name.endswith(".exe"):
|
|
142
|
+
return (name + ".exe", name)
|
|
143
|
+
return (name,)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _candidate_names() -> Iterator[str]:
|
|
147
|
+
for name in BINARY_NAMES:
|
|
148
|
+
yield from _with_platform_suffix(name)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _packaged_candidate_names() -> Iterator[str]:
|
|
152
|
+
for name in packaged_binary_names():
|
|
153
|
+
yield name
|
|
154
|
+
if "windows-" in name and not name.endswith(".exe"):
|
|
155
|
+
yield name + ".exe"
|
|
156
|
+
elif os.name == "nt" and not name.endswith(".exe"):
|
|
157
|
+
yield name + ".exe"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _is_executable_file(path: Path) -> bool:
|
|
161
|
+
return path.is_file() and os.access(path, os.X_OK)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def candidate_binary_paths() -> Iterator[Path]:
|
|
165
|
+
"""Yield environment, packaged, and development Rust binary candidates."""
|
|
166
|
+
|
|
167
|
+
env_value = os.environ.get(ENV_BINARY)
|
|
168
|
+
if env_value:
|
|
169
|
+
yield Path(env_value).expanduser()
|
|
170
|
+
|
|
171
|
+
package_dir = Path(__file__).resolve().parent
|
|
172
|
+
for name in _packaged_candidate_names():
|
|
173
|
+
yield package_dir / "_bin" / name
|
|
174
|
+
|
|
175
|
+
# Development checkout: src/codetool_explore/rust_backend.py -> repo root
|
|
176
|
+
repo_root = package_dir.parents[1]
|
|
177
|
+
for profile in ("release", "debug"):
|
|
178
|
+
for name in _candidate_names():
|
|
179
|
+
yield repo_root / "rust" / "target" / profile / name
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def find_rust_binary() -> str | None:
|
|
183
|
+
"""Find the optional Rust CLI binary, if available."""
|
|
184
|
+
|
|
185
|
+
for path in candidate_binary_paths():
|
|
186
|
+
if _is_executable_file(path):
|
|
187
|
+
return str(path)
|
|
188
|
+
|
|
189
|
+
for name in _candidate_names():
|
|
190
|
+
found = shutil.which(name)
|
|
191
|
+
if found:
|
|
192
|
+
return found
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _append_patterns(args: list[str], flag: str, patterns: Iterable[str]) -> None:
|
|
198
|
+
for pattern in patterns:
|
|
199
|
+
args.extend((flag, pattern))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def search_rust(
|
|
203
|
+
pattern: str,
|
|
204
|
+
root: RootInput = ".",
|
|
205
|
+
*,
|
|
206
|
+
regex: bool = False,
|
|
207
|
+
target: str = "content",
|
|
208
|
+
path_scope: str = "path",
|
|
209
|
+
glob: str | Iterable[str] | None = None,
|
|
210
|
+
exclude: str | Iterable[str] | None = None,
|
|
211
|
+
case: str = "smart",
|
|
212
|
+
mode: str = "files",
|
|
213
|
+
context_lines: int = 0,
|
|
214
|
+
limit: int = 50,
|
|
215
|
+
cursor: str | int | None = None,
|
|
216
|
+
binary: str | None = None,
|
|
217
|
+
) -> dict[str, object]:
|
|
218
|
+
"""Search workspace content, paths, or content/path union via Rust CLI."""
|
|
219
|
+
|
|
220
|
+
if not isinstance(pattern, str):
|
|
221
|
+
raise ExploreArgumentError("pattern must be a string")
|
|
222
|
+
normalised_target = normalize_target(target)
|
|
223
|
+
if pattern == "" and normalised_target in {"content", "content_or_path"}:
|
|
224
|
+
raise ExplorePatternError("pattern must not be empty")
|
|
225
|
+
root_set = normalize_search_roots(root)
|
|
226
|
+
|
|
227
|
+
normalised_mode = normalize_mode(mode)
|
|
228
|
+
if normalised_mode == "snippets" and normalised_target == "path":
|
|
229
|
+
raise ExploreArgumentError(
|
|
230
|
+
"mode='snippets' is not supported for target='path'; "
|
|
231
|
+
"use target='content' or target='content_or_path'"
|
|
232
|
+
)
|
|
233
|
+
normalised_path_scope = normalize_path_scope(path_scope)
|
|
234
|
+
resolve_case(str(case or "smart").lower(), pattern)
|
|
235
|
+
safe_limit = normalize_limit(limit)
|
|
236
|
+
try:
|
|
237
|
+
safe_context_lines = max(0, int(context_lines or 0))
|
|
238
|
+
except (TypeError, ValueError) as exc:
|
|
239
|
+
raise ExploreArgumentError(
|
|
240
|
+
"context_lines must be a non-negative integer"
|
|
241
|
+
) from exc
|
|
242
|
+
glob_patterns = normalize_patterns(glob)
|
|
243
|
+
exclude_patterns = normalize_patterns(exclude)
|
|
244
|
+
|
|
245
|
+
executable = binary or find_rust_binary()
|
|
246
|
+
if not executable:
|
|
247
|
+
raise RustBackendUnavailable(
|
|
248
|
+
f"Rust backend unavailable; set {ENV_BINARY} or build rust/ with cargo"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
args = [
|
|
252
|
+
executable,
|
|
253
|
+
"--pattern",
|
|
254
|
+
pattern,
|
|
255
|
+
"--mode",
|
|
256
|
+
normalised_mode,
|
|
257
|
+
"--target",
|
|
258
|
+
normalised_target,
|
|
259
|
+
"--path-scope",
|
|
260
|
+
normalised_path_scope,
|
|
261
|
+
"--case",
|
|
262
|
+
str(case or "smart"),
|
|
263
|
+
"--limit",
|
|
264
|
+
str(safe_limit),
|
|
265
|
+
"--context-lines",
|
|
266
|
+
str(safe_context_lines),
|
|
267
|
+
]
|
|
268
|
+
for search_root in root_set.roots:
|
|
269
|
+
args.extend(("--root", search_root.raw))
|
|
270
|
+
if regex:
|
|
271
|
+
args.append("--regex")
|
|
272
|
+
if cursor not in (None, ""):
|
|
273
|
+
args.extend(("--cursor", str(cursor)))
|
|
274
|
+
_append_patterns(args, "--glob", glob_patterns)
|
|
275
|
+
_append_patterns(args, "--exclude", exclude_patterns)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
completed = subprocess.run(
|
|
279
|
+
args,
|
|
280
|
+
check=False,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
text=True,
|
|
283
|
+
encoding="utf-8",
|
|
284
|
+
timeout=60,
|
|
285
|
+
)
|
|
286
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
287
|
+
raise ExploreBackendError(f"Rust backend failed: {exc}") from exc
|
|
288
|
+
if completed.returncode != 0:
|
|
289
|
+
message = (
|
|
290
|
+
completed.stderr.strip()
|
|
291
|
+
or completed.stdout.strip()
|
|
292
|
+
or "Rust backend failed"
|
|
293
|
+
)
|
|
294
|
+
if "invalid regex" in message or "empty spans" in message:
|
|
295
|
+
raise ExplorePatternError(message)
|
|
296
|
+
raise ExploreBackendError(message)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
result = json.loads(completed.stdout)
|
|
300
|
+
except json.JSONDecodeError as exc:
|
|
301
|
+
raise ExploreBackendError("Rust backend returned invalid JSON") from exc
|
|
302
|
+
|
|
303
|
+
if not isinstance(result, dict):
|
|
304
|
+
raise ExploreBackendError("Rust backend returned a non-object JSON response")
|
|
305
|
+
|
|
306
|
+
result["backend"] = "rust"
|
|
307
|
+
result["root"] = root_set.display
|
|
308
|
+
return result
|