ripperdoc 0.2.6__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.
- ripperdoc/__init__.py +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""Path ignore utilities for Ripperdoc.
|
|
2
|
+
|
|
3
|
+
This module implements comprehensive path ignore checking based on:
|
|
4
|
+
1. Default ignore patterns (binary files, build outputs, etc.)
|
|
5
|
+
2. .gitignore patterns
|
|
6
|
+
3. Project configuration ignore patterns
|
|
7
|
+
4. User-defined ignore patterns
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
|
|
17
|
+
from ripperdoc.utils.git_utils import (
|
|
18
|
+
get_git_root,
|
|
19
|
+
is_git_repository,
|
|
20
|
+
read_gitignore_patterns,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Default Ignore Patterns (System-level)
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
# These patterns are always ignored for safety and performance
|
|
29
|
+
DEFAULT_IGNORE_PATTERNS: List[str] = [
|
|
30
|
+
# Version control
|
|
31
|
+
".git/",
|
|
32
|
+
".svn/",
|
|
33
|
+
".hg/",
|
|
34
|
+
# IDE and editor
|
|
35
|
+
".idea/",
|
|
36
|
+
".vscode/",
|
|
37
|
+
"*.swp",
|
|
38
|
+
"*.swo",
|
|
39
|
+
"*~",
|
|
40
|
+
# Ripperdoc config
|
|
41
|
+
".ripperdoc/",
|
|
42
|
+
".claude/",
|
|
43
|
+
# Build and cache directories
|
|
44
|
+
".parcel-cache/",
|
|
45
|
+
".pytest_cache/",
|
|
46
|
+
".nuxt/",
|
|
47
|
+
".next/",
|
|
48
|
+
".sass-cache/",
|
|
49
|
+
"__pycache__/",
|
|
50
|
+
"*.pyc",
|
|
51
|
+
"*.pyo",
|
|
52
|
+
# Node.js
|
|
53
|
+
"node_modules/",
|
|
54
|
+
# Python environments
|
|
55
|
+
"venv/",
|
|
56
|
+
".venv/",
|
|
57
|
+
"env/",
|
|
58
|
+
".env/",
|
|
59
|
+
".tox/",
|
|
60
|
+
# Java/Gradle
|
|
61
|
+
".gradle/",
|
|
62
|
+
"build/",
|
|
63
|
+
"target/",
|
|
64
|
+
# .NET
|
|
65
|
+
"bin/",
|
|
66
|
+
"obj/",
|
|
67
|
+
# Rust
|
|
68
|
+
"target/",
|
|
69
|
+
# Go
|
|
70
|
+
"vendor/",
|
|
71
|
+
# Ruby
|
|
72
|
+
"vendor/bundle/",
|
|
73
|
+
# Dart/Flutter
|
|
74
|
+
".dart_tool/",
|
|
75
|
+
".pub-cache/",
|
|
76
|
+
# Elixir
|
|
77
|
+
"_build/",
|
|
78
|
+
"deps/",
|
|
79
|
+
# Haskell
|
|
80
|
+
"dist-newstyle/",
|
|
81
|
+
# JavaScript/TypeScript build outputs
|
|
82
|
+
"dist/",
|
|
83
|
+
# Deno
|
|
84
|
+
".deno/",
|
|
85
|
+
# Bower (legacy)
|
|
86
|
+
"bower_components/",
|
|
87
|
+
# Image files
|
|
88
|
+
"*.png",
|
|
89
|
+
"*.jpg",
|
|
90
|
+
"*.jpeg",
|
|
91
|
+
"*.gif",
|
|
92
|
+
"*.bmp",
|
|
93
|
+
"*.ico",
|
|
94
|
+
"*.webp",
|
|
95
|
+
"*.svg",
|
|
96
|
+
"*.psd",
|
|
97
|
+
"*.ai",
|
|
98
|
+
"*.eps",
|
|
99
|
+
"*.tiff",
|
|
100
|
+
"*.tif",
|
|
101
|
+
"*.avif",
|
|
102
|
+
"*.heic",
|
|
103
|
+
"*.heif",
|
|
104
|
+
# Video files
|
|
105
|
+
"*.mp4",
|
|
106
|
+
"*.avi",
|
|
107
|
+
"*.mkv",
|
|
108
|
+
"*.mov",
|
|
109
|
+
"*.wmv",
|
|
110
|
+
"*.flv",
|
|
111
|
+
"*.webm",
|
|
112
|
+
"*.m4v",
|
|
113
|
+
"*.mpeg",
|
|
114
|
+
"*.mpg",
|
|
115
|
+
"*.3gp",
|
|
116
|
+
"*.3g2",
|
|
117
|
+
# Audio files
|
|
118
|
+
"*.mp3",
|
|
119
|
+
"*.wav",
|
|
120
|
+
"*.flac",
|
|
121
|
+
"*.aac",
|
|
122
|
+
"*.ogg",
|
|
123
|
+
"*.wma",
|
|
124
|
+
"*.m4a",
|
|
125
|
+
"*.aiff",
|
|
126
|
+
"*.asf",
|
|
127
|
+
# Compressed archives
|
|
128
|
+
"*.zip",
|
|
129
|
+
"*.tar",
|
|
130
|
+
"*.gz",
|
|
131
|
+
"*.bz2",
|
|
132
|
+
"*.xz",
|
|
133
|
+
"*.7z",
|
|
134
|
+
"*.rar",
|
|
135
|
+
"*.tgz",
|
|
136
|
+
# Executable and binary files
|
|
137
|
+
"*.exe",
|
|
138
|
+
"*.dll",
|
|
139
|
+
"*.so",
|
|
140
|
+
"*.dylib",
|
|
141
|
+
"*.bin",
|
|
142
|
+
"*.app",
|
|
143
|
+
"*.dmg",
|
|
144
|
+
"*.msi",
|
|
145
|
+
"*.deb",
|
|
146
|
+
"*.rpm",
|
|
147
|
+
# Database files
|
|
148
|
+
"*.db",
|
|
149
|
+
"*.sqlite",
|
|
150
|
+
"*.sqlite3",
|
|
151
|
+
"*.parquet",
|
|
152
|
+
"*.orc",
|
|
153
|
+
"*.arrow",
|
|
154
|
+
# GIS data
|
|
155
|
+
"*.shp",
|
|
156
|
+
"*.kmz",
|
|
157
|
+
"*.kml",
|
|
158
|
+
"*.dem",
|
|
159
|
+
"*.las",
|
|
160
|
+
"*.laz",
|
|
161
|
+
# CAD/Design files
|
|
162
|
+
"*.dwg",
|
|
163
|
+
"*.dxf",
|
|
164
|
+
# PDF (can be read but often large)
|
|
165
|
+
# "*.pdf", # Keeping PDF readable since it's often documentation
|
|
166
|
+
# Font files
|
|
167
|
+
"*.ttf",
|
|
168
|
+
"*.otf",
|
|
169
|
+
"*.woff",
|
|
170
|
+
"*.woff2",
|
|
171
|
+
"*.eot",
|
|
172
|
+
# Lock files (usually large and auto-generated)
|
|
173
|
+
"package-lock.json",
|
|
174
|
+
"yarn.lock",
|
|
175
|
+
"pnpm-lock.yaml",
|
|
176
|
+
"poetry.lock",
|
|
177
|
+
"Cargo.lock",
|
|
178
|
+
"Gemfile.lock",
|
|
179
|
+
"composer.lock",
|
|
180
|
+
# Large data files
|
|
181
|
+
"*.csv", # Can be very large
|
|
182
|
+
"*.jsonl",
|
|
183
|
+
"*.ndjson",
|
|
184
|
+
# ML model files
|
|
185
|
+
"*.pt",
|
|
186
|
+
"*.pth",
|
|
187
|
+
"*.onnx",
|
|
188
|
+
"*.h5",
|
|
189
|
+
"*.hdf5",
|
|
190
|
+
"*.safetensors",
|
|
191
|
+
"*.ckpt",
|
|
192
|
+
"*.pkl",
|
|
193
|
+
"*.pickle",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
# Directories that are always skipped during traversal (fast path)
|
|
197
|
+
IGNORED_DIRECTORIES: Set[str] = {
|
|
198
|
+
"node_modules",
|
|
199
|
+
"vendor/bundle",
|
|
200
|
+
"vendor",
|
|
201
|
+
"venv",
|
|
202
|
+
"env",
|
|
203
|
+
".venv",
|
|
204
|
+
".env",
|
|
205
|
+
".tox",
|
|
206
|
+
"target",
|
|
207
|
+
"build",
|
|
208
|
+
".gradle",
|
|
209
|
+
"packages",
|
|
210
|
+
"bin",
|
|
211
|
+
"obj",
|
|
212
|
+
".build",
|
|
213
|
+
".dart_tool",
|
|
214
|
+
".pub-cache",
|
|
215
|
+
"_build",
|
|
216
|
+
"deps",
|
|
217
|
+
"dist",
|
|
218
|
+
"dist-newstyle",
|
|
219
|
+
".deno",
|
|
220
|
+
"bower_components",
|
|
221
|
+
"__pycache__",
|
|
222
|
+
".git",
|
|
223
|
+
".svn",
|
|
224
|
+
".hg",
|
|
225
|
+
".idea",
|
|
226
|
+
".vscode",
|
|
227
|
+
".ripperdoc",
|
|
228
|
+
".claude",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# =============================================================================
|
|
233
|
+
# Pattern Matching Implementation
|
|
234
|
+
# =============================================================================
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _compile_pattern(pattern: str) -> re.Pattern[str]:
|
|
238
|
+
"""Compile a gitignore-style pattern to a regex.
|
|
239
|
+
|
|
240
|
+
This supports basic gitignore syntax:
|
|
241
|
+
- * matches anything except /
|
|
242
|
+
- ** matches anything including /
|
|
243
|
+
- ? matches any single character
|
|
244
|
+
- [abc] character classes
|
|
245
|
+
- ! at start negates the pattern
|
|
246
|
+
- / at end matches directories only
|
|
247
|
+
- / at start anchors to root
|
|
248
|
+
"""
|
|
249
|
+
# Remove trailing slashes for matching (we handle directory-only patterns separately)
|
|
250
|
+
is_dir_only = pattern.endswith("/")
|
|
251
|
+
if is_dir_only:
|
|
252
|
+
pattern = pattern[:-1]
|
|
253
|
+
|
|
254
|
+
# Check if pattern is anchored to root
|
|
255
|
+
is_anchored = pattern.startswith("/")
|
|
256
|
+
if is_anchored:
|
|
257
|
+
pattern = pattern[1:]
|
|
258
|
+
|
|
259
|
+
# Escape special regex characters (except our wildcards)
|
|
260
|
+
regex = ""
|
|
261
|
+
i = 0
|
|
262
|
+
while i < len(pattern):
|
|
263
|
+
c = pattern[i]
|
|
264
|
+
|
|
265
|
+
if c == "*":
|
|
266
|
+
# Check for **
|
|
267
|
+
if i + 1 < len(pattern) and pattern[i + 1] == "*":
|
|
268
|
+
# ** matches anything including path separators
|
|
269
|
+
if i + 2 < len(pattern) and pattern[i + 2] == "/":
|
|
270
|
+
regex += "(?:.*/)?"
|
|
271
|
+
i += 3
|
|
272
|
+
continue
|
|
273
|
+
else:
|
|
274
|
+
regex += ".*"
|
|
275
|
+
i += 2
|
|
276
|
+
continue
|
|
277
|
+
else:
|
|
278
|
+
# * matches anything except /
|
|
279
|
+
regex += "[^/]*"
|
|
280
|
+
elif c == "?":
|
|
281
|
+
regex += "[^/]"
|
|
282
|
+
elif c == "[":
|
|
283
|
+
# Find closing bracket
|
|
284
|
+
j = i + 1
|
|
285
|
+
if j < len(pattern) and pattern[j] in "!^":
|
|
286
|
+
j += 1
|
|
287
|
+
while j < len(pattern) and pattern[j] != "]":
|
|
288
|
+
j += 1
|
|
289
|
+
if j < len(pattern):
|
|
290
|
+
regex += pattern[i:j + 1]
|
|
291
|
+
i = j
|
|
292
|
+
else:
|
|
293
|
+
regex += re.escape(c)
|
|
294
|
+
elif c in ".^$+{}|()":
|
|
295
|
+
regex += re.escape(c)
|
|
296
|
+
else:
|
|
297
|
+
regex += c
|
|
298
|
+
|
|
299
|
+
i += 1
|
|
300
|
+
|
|
301
|
+
# Build final regex
|
|
302
|
+
if is_anchored:
|
|
303
|
+
final_regex = f"^{regex}"
|
|
304
|
+
else:
|
|
305
|
+
# Non-anchored patterns can match anywhere in the path
|
|
306
|
+
final_regex = f"(?:^|/){regex}"
|
|
307
|
+
|
|
308
|
+
if is_dir_only:
|
|
309
|
+
final_regex += "(?:/|$)"
|
|
310
|
+
else:
|
|
311
|
+
final_regex += "(?:/.*)?$"
|
|
312
|
+
|
|
313
|
+
return re.compile(final_regex)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class IgnoreFilter:
|
|
317
|
+
"""A filter for checking if paths should be ignored.
|
|
318
|
+
|
|
319
|
+
Uses gitignore-style pattern matching.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def __init__(self) -> None:
|
|
323
|
+
self._patterns: List[Tuple[re.Pattern[str], bool]] = [] # (pattern, is_negation)
|
|
324
|
+
|
|
325
|
+
def add(self, patterns: List[str]) -> "IgnoreFilter":
|
|
326
|
+
"""Add patterns to the filter."""
|
|
327
|
+
for pattern in patterns:
|
|
328
|
+
pattern = pattern.strip()
|
|
329
|
+
if not pattern or pattern.startswith("#"):
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
is_negation = pattern.startswith("!")
|
|
333
|
+
if is_negation:
|
|
334
|
+
pattern = pattern[1:]
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
compiled = _compile_pattern(pattern)
|
|
338
|
+
self._patterns.append((compiled, is_negation))
|
|
339
|
+
except re.error:
|
|
340
|
+
# Skip invalid patterns
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
def ignores(self, path: str) -> bool:
|
|
346
|
+
"""Check if a path should be ignored.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
path: Relative path to check (using / as separator)
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if the path should be ignored
|
|
353
|
+
"""
|
|
354
|
+
# Normalize path
|
|
355
|
+
path = path.replace("\\", "/").strip("/")
|
|
356
|
+
|
|
357
|
+
result = False
|
|
358
|
+
for pattern, is_negation in self._patterns:
|
|
359
|
+
if pattern.search(path):
|
|
360
|
+
result = not is_negation
|
|
361
|
+
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
def test(self, path: str) -> Dict[str, any]:
|
|
365
|
+
"""Check if a path should be ignored and return details.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dict with 'ignored' bool and 'rule' if matched
|
|
369
|
+
"""
|
|
370
|
+
path = path.replace("\\", "/").strip("/")
|
|
371
|
+
|
|
372
|
+
result = {"ignored": False, "rule": None}
|
|
373
|
+
|
|
374
|
+
for pattern, is_negation in self._patterns:
|
|
375
|
+
if pattern.search(path):
|
|
376
|
+
result["ignored"] = not is_negation
|
|
377
|
+
result["rule"] = {"pattern": pattern.pattern, "negation": is_negation}
|
|
378
|
+
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# =============================================================================
|
|
383
|
+
# Ignore Pattern Management
|
|
384
|
+
# =============================================================================
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def parse_ignore_pattern(pattern: str, settings_path: Optional[Path] = None) -> Tuple[str, Optional[Path]]:
|
|
388
|
+
"""Parse an ignore pattern and return (relative_pattern, root_path).
|
|
389
|
+
|
|
390
|
+
Supports prefixes:
|
|
391
|
+
- // - Global pattern (from filesystem root)
|
|
392
|
+
- ~/ - Pattern relative to home directory
|
|
393
|
+
- / - Pattern relative to settings file directory
|
|
394
|
+
- (no prefix) - Pattern applies to any directory
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
pattern: The ignore pattern to parse
|
|
398
|
+
settings_path: Path to the settings file (for / prefix patterns)
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Tuple of (relative_pattern, root_path or None)
|
|
402
|
+
"""
|
|
403
|
+
pattern = pattern.strip()
|
|
404
|
+
|
|
405
|
+
# // - Global pattern from filesystem root
|
|
406
|
+
if pattern.startswith("//"):
|
|
407
|
+
return pattern[1:], Path("/")
|
|
408
|
+
|
|
409
|
+
# ~/ - Pattern relative to home directory
|
|
410
|
+
if pattern.startswith("~/"):
|
|
411
|
+
return pattern[2:], Path.home()
|
|
412
|
+
|
|
413
|
+
# / - Pattern relative to settings file directory
|
|
414
|
+
if pattern.startswith("/") and settings_path:
|
|
415
|
+
# Determine if settings_path is a file or directory based on suffix
|
|
416
|
+
# If it has a file-like suffix (e.g., .json), treat as file
|
|
417
|
+
if settings_path.suffix:
|
|
418
|
+
return pattern[1:], settings_path.parent
|
|
419
|
+
else:
|
|
420
|
+
return pattern[1:], settings_path
|
|
421
|
+
|
|
422
|
+
# No prefix - applies to any directory
|
|
423
|
+
return pattern, None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def build_ignore_filter(
|
|
427
|
+
root_path: Path,
|
|
428
|
+
user_patterns: Optional[List[str]] = None,
|
|
429
|
+
project_patterns: Optional[List[str]] = None,
|
|
430
|
+
include_defaults: bool = True,
|
|
431
|
+
include_gitignore: bool = True,
|
|
432
|
+
) -> IgnoreFilter:
|
|
433
|
+
"""Build an ignore filter with all applicable patterns.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
root_path: The root path for pattern matching
|
|
437
|
+
user_patterns: User-provided patterns
|
|
438
|
+
project_patterns: Project configuration patterns
|
|
439
|
+
include_defaults: Whether to include default ignore patterns
|
|
440
|
+
include_gitignore: Whether to include .gitignore patterns
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Configured IgnoreFilter instance
|
|
444
|
+
"""
|
|
445
|
+
ignore_filter = IgnoreFilter()
|
|
446
|
+
all_patterns: List[str] = []
|
|
447
|
+
|
|
448
|
+
# 1. Add default patterns
|
|
449
|
+
if include_defaults:
|
|
450
|
+
all_patterns.extend(DEFAULT_IGNORE_PATTERNS)
|
|
451
|
+
|
|
452
|
+
# 2. Add gitignore patterns
|
|
453
|
+
if include_gitignore and is_git_repository(root_path):
|
|
454
|
+
gitignore_patterns = read_gitignore_patterns(root_path)
|
|
455
|
+
all_patterns.extend(gitignore_patterns)
|
|
456
|
+
|
|
457
|
+
# 3. Add project patterns
|
|
458
|
+
if project_patterns:
|
|
459
|
+
all_patterns.extend(project_patterns)
|
|
460
|
+
|
|
461
|
+
# 4. Add user patterns
|
|
462
|
+
if user_patterns:
|
|
463
|
+
all_patterns.extend(user_patterns)
|
|
464
|
+
|
|
465
|
+
ignore_filter.add(all_patterns)
|
|
466
|
+
return ignore_filter
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def get_project_ignore_patterns() -> List[str]:
|
|
470
|
+
"""Get ignore patterns from project configuration.
|
|
471
|
+
|
|
472
|
+
Returns patterns from ProjectConfig.ignore_patterns if configured.
|
|
473
|
+
"""
|
|
474
|
+
try:
|
|
475
|
+
from ripperdoc.core.config import config_manager
|
|
476
|
+
|
|
477
|
+
project_config = config_manager.get_project_config()
|
|
478
|
+
return getattr(project_config, "ignore_patterns", []) or []
|
|
479
|
+
except (ImportError, AttributeError):
|
|
480
|
+
return []
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# =============================================================================
|
|
484
|
+
# Path Checking Functions
|
|
485
|
+
# =============================================================================
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def is_path_ignored(
|
|
489
|
+
file_path: Path,
|
|
490
|
+
root_path: Optional[Path] = None,
|
|
491
|
+
ignore_filter: Optional[IgnoreFilter] = None,
|
|
492
|
+
) -> bool:
|
|
493
|
+
"""Check if a file path should be ignored.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
file_path: The file path to check
|
|
497
|
+
root_path: The root path for relative matching (defaults to git root or cwd)
|
|
498
|
+
ignore_filter: Pre-built ignore filter (if None, builds a new one)
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
True if the path should be ignored
|
|
502
|
+
"""
|
|
503
|
+
# Resolve paths
|
|
504
|
+
file_path = Path(file_path)
|
|
505
|
+
if not file_path.is_absolute():
|
|
506
|
+
from ripperdoc.utils.safe_get_cwd import safe_get_cwd
|
|
507
|
+
file_path = Path(safe_get_cwd()) / file_path
|
|
508
|
+
|
|
509
|
+
file_path = file_path.resolve()
|
|
510
|
+
|
|
511
|
+
# Determine root path
|
|
512
|
+
if root_path is None:
|
|
513
|
+
root_path = get_git_root(file_path.parent)
|
|
514
|
+
if root_path is None:
|
|
515
|
+
from ripperdoc.utils.safe_get_cwd import safe_get_cwd
|
|
516
|
+
root_path = Path(safe_get_cwd())
|
|
517
|
+
|
|
518
|
+
root_path = root_path.resolve()
|
|
519
|
+
|
|
520
|
+
# Get relative path
|
|
521
|
+
try:
|
|
522
|
+
rel_path = file_path.relative_to(root_path).as_posix()
|
|
523
|
+
except ValueError:
|
|
524
|
+
# Path is not under root, not ignored
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
# Build filter if not provided
|
|
528
|
+
if ignore_filter is None:
|
|
529
|
+
project_patterns = get_project_ignore_patterns()
|
|
530
|
+
ignore_filter = build_ignore_filter(
|
|
531
|
+
root_path,
|
|
532
|
+
project_patterns=project_patterns,
|
|
533
|
+
include_defaults=True,
|
|
534
|
+
include_gitignore=True,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
return ignore_filter.ignores(rel_path)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def is_directory_ignored(dir_name: str) -> bool:
|
|
541
|
+
"""Quick check if a directory name is in the always-ignored list.
|
|
542
|
+
|
|
543
|
+
This is a fast path for directory traversal.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
dir_name: The directory name (not full path)
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
True if the directory should always be skipped
|
|
550
|
+
"""
|
|
551
|
+
return dir_name in IGNORED_DIRECTORIES
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def should_skip_path(
|
|
555
|
+
path: Path,
|
|
556
|
+
root_path: Path,
|
|
557
|
+
ignore_filter: Optional[IgnoreFilter] = None,
|
|
558
|
+
skip_hidden: bool = True,
|
|
559
|
+
) -> bool:
|
|
560
|
+
"""Check if a path should be skipped during traversal.
|
|
561
|
+
|
|
562
|
+
Combines multiple checks:
|
|
563
|
+
- Hidden files (starting with .)
|
|
564
|
+
- Always-ignored directories
|
|
565
|
+
- Ignore filter patterns
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
path: The path to check
|
|
569
|
+
root_path: The root path for relative matching
|
|
570
|
+
ignore_filter: Pre-built ignore filter
|
|
571
|
+
skip_hidden: Whether to skip hidden files
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
True if the path should be skipped
|
|
575
|
+
"""
|
|
576
|
+
name = path.name
|
|
577
|
+
|
|
578
|
+
# Skip hidden files
|
|
579
|
+
if skip_hidden and name.startswith("."):
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
# Quick check for always-ignored directories
|
|
583
|
+
if path.is_dir() and is_directory_ignored(name):
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
# Check against ignore filter
|
|
587
|
+
if ignore_filter is not None:
|
|
588
|
+
try:
|
|
589
|
+
rel_path = path.relative_to(root_path).as_posix()
|
|
590
|
+
if ignore_filter.ignores(rel_path):
|
|
591
|
+
return True
|
|
592
|
+
except ValueError:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# =============================================================================
|
|
599
|
+
# Integration with File Tools
|
|
600
|
+
# =============================================================================
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def check_path_for_tool(
|
|
604
|
+
file_path: Path,
|
|
605
|
+
tool_name: str = "unknown",
|
|
606
|
+
warn_only: bool = True,
|
|
607
|
+
) -> Tuple[bool, Optional[str]]:
|
|
608
|
+
"""Check if a path should be accessible for a tool.
|
|
609
|
+
|
|
610
|
+
This is designed to be called from file tools (Read, Edit, Write)
|
|
611
|
+
to warn or block access to ignored paths.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
file_path: The file path to check
|
|
615
|
+
tool_name: Name of the calling tool (for messages)
|
|
616
|
+
warn_only: If True, return warning message; if False, block access
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Tuple of (should_proceed, warning_message)
|
|
620
|
+
- should_proceed: True if the operation should continue
|
|
621
|
+
- warning_message: Warning or error message if path is ignored
|
|
622
|
+
"""
|
|
623
|
+
if is_path_ignored(file_path):
|
|
624
|
+
file_name = file_path.name
|
|
625
|
+
|
|
626
|
+
# Check why it's ignored
|
|
627
|
+
reasons = []
|
|
628
|
+
|
|
629
|
+
# Check if it's a binary/media file
|
|
630
|
+
suffix = file_path.suffix.lower()
|
|
631
|
+
binary_extensions = {
|
|
632
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp",
|
|
633
|
+
".mp4", ".avi", ".mkv", ".mov", ".mp3", ".wav", ".flac",
|
|
634
|
+
".zip", ".tar", ".gz", ".7z", ".rar",
|
|
635
|
+
".exe", ".dll", ".so", ".dylib",
|
|
636
|
+
".db", ".sqlite", ".parquet",
|
|
637
|
+
".ttf", ".otf", ".woff",
|
|
638
|
+
}
|
|
639
|
+
if suffix in binary_extensions:
|
|
640
|
+
reasons.append("binary/media file")
|
|
641
|
+
|
|
642
|
+
# Check if it's in an ignored directory
|
|
643
|
+
for part in file_path.parts:
|
|
644
|
+
if is_directory_ignored(part):
|
|
645
|
+
reasons.append(f"inside '{part}' directory")
|
|
646
|
+
break
|
|
647
|
+
|
|
648
|
+
reason_str = ", ".join(reasons) if reasons else "matches ignore pattern"
|
|
649
|
+
|
|
650
|
+
if warn_only:
|
|
651
|
+
message = (
|
|
652
|
+
f"Warning: '{file_name}' is typically ignored ({reason_str}). "
|
|
653
|
+
f"Proceeding with {tool_name} operation."
|
|
654
|
+
)
|
|
655
|
+
return True, message
|
|
656
|
+
else:
|
|
657
|
+
message = (
|
|
658
|
+
f"Access denied: '{file_name}' is in the ignore list ({reason_str}). "
|
|
659
|
+
f"This file type is not meant to be accessed by {tool_name}."
|
|
660
|
+
)
|
|
661
|
+
return False, message
|
|
662
|
+
|
|
663
|
+
return True, None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
__all__ = [
|
|
667
|
+
"DEFAULT_IGNORE_PATTERNS",
|
|
668
|
+
"IGNORED_DIRECTORIES",
|
|
669
|
+
"IgnoreFilter",
|
|
670
|
+
"build_ignore_filter",
|
|
671
|
+
"check_path_for_tool",
|
|
672
|
+
"get_project_ignore_patterns",
|
|
673
|
+
"is_directory_ignored",
|
|
674
|
+
"is_path_ignored",
|
|
675
|
+
"parse_ignore_pattern",
|
|
676
|
+
"should_skip_path",
|
|
677
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Filesystem path helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _legacy_sanitize_project_path(project_path: Path) -> str:
|
|
11
|
+
"""Legacy sanitizer that strips non-alphanumeric characters."""
|
|
12
|
+
normalized = str(project_path.resolve())
|
|
13
|
+
return re.sub(r"[^a-zA-Z0-9]+", "-", normalized).strip("-") or "project"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sanitize_project_path(project_path: Path) -> str:
|
|
17
|
+
"""Make a project path safe for directory names and avoid collisions.
|
|
18
|
+
|
|
19
|
+
Non-alphanumeric characters (including non-ASCII) are replaced with "-".
|
|
20
|
+
A short hash of the full resolved path is appended to prevent collisions
|
|
21
|
+
between different paths that would otherwise sanitize to the same string.
|
|
22
|
+
"""
|
|
23
|
+
normalized = str(project_path.resolve())
|
|
24
|
+
safe = _legacy_sanitize_project_path(project_path)
|
|
25
|
+
digest = hashlib.sha1(normalized.encode("utf-8")).hexdigest()[:8]
|
|
26
|
+
return f"{safe}-{digest}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def project_storage_dir(base_dir: Path, project_path: Path, ensure: bool = False) -> Path:
|
|
30
|
+
"""Return a storage directory path for a project, with legacy fallback.
|
|
31
|
+
|
|
32
|
+
Prefers a hashed, collision-safe name but will reuse an existing legacy
|
|
33
|
+
directory (pre-hash) to avoid stranding older data.
|
|
34
|
+
"""
|
|
35
|
+
hashed_name = sanitize_project_path(project_path)
|
|
36
|
+
legacy_name = _legacy_sanitize_project_path(project_path)
|
|
37
|
+
|
|
38
|
+
hashed_dir = base_dir / hashed_name
|
|
39
|
+
legacy_dir = base_dir / legacy_name
|
|
40
|
+
|
|
41
|
+
chosen = hashed_dir if hashed_dir.exists() or not legacy_dir.exists() else legacy_dir
|
|
42
|
+
|
|
43
|
+
if ensure:
|
|
44
|
+
chosen.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
return chosen
|