skydeckai-code 0.1.38__py3-none-any.whl → 0.1.40__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.
- {skydeckai_code-0.1.38.dist-info → skydeckai_code-0.1.40.dist-info}/METADATA +6 -246
- {skydeckai_code-0.1.38.dist-info → skydeckai_code-0.1.40.dist-info}/RECORD +10 -10
- src/aidd/tools/code_execution.py +135 -98
- src/aidd/tools/code_tools.py +53 -7
- src/aidd/tools/directory_tools.py +285 -140
- src/aidd/tools/file_tools.py +90 -21
- src/aidd/tools/system_tools.py +4 -1
- {skydeckai_code-0.1.38.dist-info → skydeckai_code-0.1.40.dist-info}/WHEEL +0 -0
- {skydeckai_code-0.1.38.dist-info → skydeckai_code-0.1.40.dist-info}/entry_points.txt +0 -0
- {skydeckai_code-0.1.38.dist-info → skydeckai_code-0.1.40.dist-info}/licenses/LICENSE +0 -0
@@ -2,9 +2,9 @@ import json
|
|
2
2
|
import os
|
3
3
|
import subprocess
|
4
4
|
from datetime import datetime
|
5
|
-
|
5
|
+
import asyncio
|
6
6
|
from mcp.types import TextContent
|
7
|
-
|
7
|
+
from pathlib import Path
|
8
8
|
from .state import state
|
9
9
|
|
10
10
|
|
@@ -12,12 +12,12 @@ def list_directory_tool():
|
|
12
12
|
return {
|
13
13
|
"name": "list_directory",
|
14
14
|
"description": "Get a detailed listing of files and directories in the specified path, including type, size, and modification "
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
"date. WHEN TO USE: When you need to explore the contents of a directory, understand what files are available, check file sizes or "
|
16
|
+
"modification dates, or locate specific files by name. WHEN NOT TO USE: When you need to read the contents of files (use read_file "
|
17
|
+
"instead), when you need a recursive listing of all subdirectories (use directory_tree instead), or when searching for files by name pattern "
|
18
|
+
"(use search_files instead). RETURNS: Text with each line containing file type ([DIR]/[FILE]), name, size (in B/KB/MB), and "
|
19
|
+
"modification date. Only works within the allowed directory. Example: Enter 'src' to list contents of the src directory, or '.' for "
|
20
|
+
"current directory.",
|
21
21
|
"inputSchema": {
|
22
22
|
"type": "object",
|
23
23
|
"properties": {
|
@@ -26,10 +26,11 @@ def list_directory_tool():
|
|
26
26
|
"description": "Path of the directory to list. Examples: '.' for current directory, 'src' for src directory, 'docs/images' for a nested directory. The path must be within the allowed workspace.",
|
27
27
|
}
|
28
28
|
},
|
29
|
-
"required": ["path"]
|
29
|
+
"required": ["path"],
|
30
30
|
},
|
31
31
|
}
|
32
32
|
|
33
|
+
|
33
34
|
async def handle_list_directory(arguments: dict):
|
34
35
|
from mcp.types import TextContent
|
35
36
|
|
@@ -77,29 +78,31 @@ async def handle_list_directory(arguments: dict):
|
|
77
78
|
except PermissionError:
|
78
79
|
raise ValueError(f"Permission denied accessing: {full_path}")
|
79
80
|
|
81
|
+
|
80
82
|
def create_directory_tool():
|
81
83
|
return {
|
82
84
|
"name": "create_directory",
|
83
85
|
"description": "Create a new directory or ensure a directory exists. "
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
86
|
+
"Can create multiple nested directories in one operation. "
|
87
|
+
"WHEN TO USE: When you need to set up project structure, organize files, create output directories before saving files, or establish a directory hierarchy. "
|
88
|
+
"WHEN NOT TO USE: When you only want to check if a directory exists (use get_file_info instead), or when trying to create directories outside the allowed workspace. "
|
89
|
+
"RETURNS: Text message confirming either that the directory was successfully created or that it already exists. "
|
90
|
+
"The operation succeeds silently if the directory already exists. "
|
91
|
+
"Only works within the allowed directory. "
|
92
|
+
"Example: Enter 'src/components' to create nested directories.",
|
91
93
|
"inputSchema": {
|
92
94
|
"type": "object",
|
93
95
|
"properties": {
|
94
96
|
"path": {
|
95
97
|
"type": "string",
|
96
|
-
"description": "Path of the directory to create. Can include nested directories which will all be created. Examples: 'logs' for a simple directory, 'src/components/buttons' for nested directories. Both absolute and relative paths are supported, but must be within the allowed workspace."
|
98
|
+
"description": "Path of the directory to create. Can include nested directories which will all be created. Examples: 'logs' for a simple directory, 'src/components/buttons' for nested directories. Both absolute and relative paths are supported, but must be within the allowed workspace.",
|
97
99
|
}
|
98
100
|
},
|
99
|
-
"required": ["path"]
|
101
|
+
"required": ["path"],
|
100
102
|
},
|
101
103
|
}
|
102
104
|
|
105
|
+
|
103
106
|
async def handle_create_directory(arguments: dict):
|
104
107
|
"""Handle creating a new directory."""
|
105
108
|
from mcp.types import TextContent
|
@@ -117,9 +120,7 @@ async def handle_create_directory(arguments: dict):
|
|
117
120
|
|
118
121
|
# Security check: ensure path is within allowed directory
|
119
122
|
if not full_path.startswith(state.allowed_directory):
|
120
|
-
raise ValueError(
|
121
|
-
f"Access denied: Path ({full_path}) must be within allowed directory ({state.allowed_directory})"
|
122
|
-
)
|
123
|
+
raise ValueError(f"Access denied: Path ({full_path}) must be within allowed directory ({state.allowed_directory})")
|
123
124
|
|
124
125
|
already_exists = os.path.exists(full_path)
|
125
126
|
|
@@ -129,72 +130,48 @@ async def handle_create_directory(arguments: dict):
|
|
129
130
|
|
130
131
|
if already_exists:
|
131
132
|
return [TextContent(type="text", text=f"Directory already exists: {path}")]
|
132
|
-
return [TextContent(
|
133
|
-
type="text",
|
134
|
-
text=f"Successfully created directory: {path}"
|
135
|
-
)]
|
133
|
+
return [TextContent(type="text", text=f"Successfully created directory: {path}")]
|
136
134
|
except PermissionError:
|
137
135
|
raise ValueError(f"Permission denied creating directory: {path}")
|
138
136
|
except Exception as e:
|
139
137
|
raise ValueError(f"Error creating directory: {str(e)}")
|
140
138
|
|
139
|
+
|
141
140
|
def directory_tree_tool():
|
142
141
|
return {
|
143
142
|
"name": "directory_tree",
|
144
143
|
"description": "Get a recursive tree view of files and directories in the specified path as a JSON structure. "
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
144
|
+
"WHEN TO USE: When you need to understand the complete structure of a directory tree, visualize the hierarchy of files and directories, or get a comprehensive overview of a project's organization. "
|
145
|
+
"Particularly useful for large projects where you need to see nested relationships. "
|
146
|
+
"WHEN NOT TO USE: When you only need a flat list of files in a single directory (use directory_listing instead), or when you're only interested in specific file types (use search_files instead). "
|
147
|
+
"RETURNS: JSON structure where each entry includes 'name', 'type' (file/directory), and 'children' for directories. "
|
148
|
+
"Files have no children array, while directories always have a children array (which may be empty). "
|
149
|
+
"The output is formatted with 2-space indentation for readability. For Git repositories, shows tracked files only. "
|
150
|
+
"Only works within the allowed directory and only for non-hidden files, or files that are not inside hidden directory. "
|
151
|
+
"If you want to show the hidden files also, use commands like execute_shell_script. "
|
152
|
+
"Example: Enter '.' for current directory, or 'src' for a specific directory.",
|
153
153
|
"inputSchema": {
|
154
154
|
"type": "object",
|
155
155
|
"properties": {
|
156
156
|
"path": {
|
157
157
|
"type": "string",
|
158
|
-
"description": "Root directory to analyze. This is the starting point for the recursive tree generation. Examples: '.' for current directory, 'src' for the src directory. Both absolute and relative paths are supported, but must be within the allowed workspace."
|
159
|
-
}
|
158
|
+
"description": "Root directory to analyze. This is the starting point for the recursive tree generation. Examples: '.' for current directory, 'src' for the src directory. Both absolute and relative paths are supported, but must be within the allowed workspace.",
|
159
|
+
},
|
160
|
+
"max_depth": {
|
161
|
+
"type": "integer",
|
162
|
+
"description": "Max depth for traversing in case of big and deeply nested directory",
|
163
|
+
"default": 3,
|
164
|
+
},
|
160
165
|
},
|
161
|
-
"required": ["path"]
|
166
|
+
"required": ["path"],
|
162
167
|
},
|
163
168
|
}
|
164
169
|
|
165
|
-
async def build_directory_tree(dir_path: str) -> dict:
|
166
|
-
"""Build directory tree as a JSON structure."""
|
167
|
-
try:
|
168
|
-
entries = list(os.scandir(dir_path))
|
169
|
-
# Sort entries by name
|
170
|
-
entries.sort(key=lambda e: e.name.lower())
|
171
|
-
|
172
|
-
result = {
|
173
|
-
"name": os.path.basename(dir_path) or dir_path,
|
174
|
-
"type": "directory",
|
175
|
-
"children": []
|
176
|
-
}
|
177
|
-
|
178
|
-
for entry in entries:
|
179
|
-
if entry.is_dir():
|
180
|
-
# Recursively process subdirectories
|
181
|
-
child_tree = await build_directory_tree(entry.path)
|
182
|
-
result["children"].append(child_tree)
|
183
|
-
else:
|
184
|
-
result["children"].append({
|
185
|
-
"name": entry.name,
|
186
|
-
"type": "file"
|
187
|
-
})
|
188
|
-
|
189
|
-
return result
|
190
|
-
except PermissionError:
|
191
|
-
raise ValueError(f"Access denied: {dir_path}")
|
192
|
-
except Exception as e:
|
193
|
-
raise ValueError(f"Error processing directory {dir_path}: {str(e)}")
|
194
170
|
|
195
171
|
async def handle_directory_tree(arguments: dict):
|
196
172
|
"""Handle building a directory tree."""
|
197
173
|
path = arguments.get("path", ".")
|
174
|
+
max_depth = arguments.get("max_depth", 3)
|
198
175
|
|
199
176
|
# Validate and get full path
|
200
177
|
full_path = os.path.abspath(os.path.join(state.allowed_directory, path))
|
@@ -205,85 +182,253 @@ async def handle_directory_tree(arguments: dict):
|
|
205
182
|
if not os.path.isdir(full_path):
|
206
183
|
raise ValueError(f"Path is not a directory: {full_path}")
|
207
184
|
|
208
|
-
|
185
|
+
"""
|
186
|
+
Idea: for git repo directory, use git ls-files to list all the files
|
187
|
+
So that we can avoid some gigantic directories like node_modules, build, dist
|
188
|
+
Else just use normal listing
|
189
|
+
1. Try git ls-files for this directory
|
190
|
+
2. If failed, identify git repo by rg and sed -> find -> python,
|
191
|
+
git ls-files then add to the visited
|
192
|
+
3. List the remaining that is not in visited using rg -> find -> python
|
193
|
+
"""
|
194
|
+
root = {"name": full_path, "type": "directory", "children": []}
|
195
|
+
dir_cache = {"": root}
|
209
196
|
try:
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
text=True,
|
216
|
-
check=True,
|
217
|
-
)
|
218
|
-
|
219
|
-
# If git command was successful
|
220
|
-
files = [f for f in result.stdout.split('\n') if f.strip()]
|
221
|
-
files.sort()
|
222
|
-
|
223
|
-
# Build tree from git files
|
224
|
-
directory_map = {}
|
225
|
-
root_name = os.path.basename(full_path) or full_path
|
226
|
-
|
227
|
-
# First pass: collect all directories and files
|
228
|
-
for file in files:
|
229
|
-
parts = file.split(os.sep)
|
230
|
-
# Add all intermediate directories
|
231
|
-
for i in range(len(parts)):
|
232
|
-
parent = os.sep.join(parts[:i])
|
233
|
-
os.sep.join(parts[:i+1])
|
234
|
-
if i < len(parts) - 1: # It's a directory
|
235
|
-
directory_map.setdefault(parent, {"dirs": set(), "files": set()})["dirs"].add(parts[i])
|
236
|
-
else: # It's a file
|
237
|
-
directory_map.setdefault(parent, {"dirs": set(), "files": set()})["files"].add(parts[i])
|
238
|
-
|
239
|
-
async def build_git_tree(current_path: str) -> dict:
|
240
|
-
dir_name = current_path.split(os.sep)[-1] if current_path else ''
|
241
|
-
result = {
|
242
|
-
"name": dir_name or root_name,
|
243
|
-
"type": "directory",
|
244
|
-
"children": [],
|
245
|
-
}
|
246
|
-
|
247
|
-
if current_path not in directory_map:
|
248
|
-
return result
|
249
|
-
|
250
|
-
entry = directory_map[current_path]
|
251
|
-
|
252
|
-
# Add directories first
|
253
|
-
for dir_name in sorted(entry["dirs"]):
|
254
|
-
child_path = os.path.join(current_path, dir_name) if current_path else dir_name
|
255
|
-
child_tree = await build_git_tree(child_path)
|
256
|
-
result["children"].append(child_tree)
|
257
|
-
|
258
|
-
# Then add files
|
259
|
-
for file_name in sorted(entry["files"]):
|
260
|
-
result["children"].append({
|
261
|
-
"name": file_name,
|
262
|
-
"type": "file",
|
263
|
-
})
|
264
|
-
|
265
|
-
return result
|
266
|
-
|
267
|
-
# Build the tree structure starting from root
|
268
|
-
tree = await build_git_tree('')
|
269
|
-
return [TextContent(type="text", text=json.dumps(tree, indent=2))]
|
270
|
-
|
271
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
272
|
-
# Git not available or not a git repository, use fallback implementation
|
197
|
+
paths = await git_ls(Path(full_path))
|
198
|
+
build_tree_from_paths(root, dir_cache, paths, max_depth)
|
199
|
+
json_tree = json.dumps(root, indent=2)
|
200
|
+
return [TextContent(type="text", text=json_tree)]
|
201
|
+
except Exception:
|
273
202
|
pass
|
274
|
-
|
275
|
-
|
276
|
-
|
203
|
+
|
204
|
+
# build the tree for git repo
|
205
|
+
try:
|
206
|
+
git_repos = await find_git_repo_async(full_path)
|
207
|
+
except Exception:
|
208
|
+
git_repos = find_git_repos_python(Path(full_path))
|
209
|
+
for git_repo in git_repos:
|
210
|
+
absolute_git_repo = Path(full_path) / git_repo
|
211
|
+
paths = []
|
212
|
+
try:
|
213
|
+
paths = await git_ls(absolute_git_repo)
|
214
|
+
except Exception:
|
215
|
+
try:
|
216
|
+
paths = await scan_path_async([], absolute_git_repo)
|
217
|
+
except Exception:
|
218
|
+
paths = scan_path([], absolute_git_repo)
|
219
|
+
finally:
|
220
|
+
paths = [git_repo / path for path in paths]
|
221
|
+
build_tree_from_paths(root, dir_cache, paths, max_depth)
|
222
|
+
|
223
|
+
# for non-git directory, do normal scan
|
224
|
+
non_git_scans = []
|
225
|
+
try:
|
226
|
+
non_git_scans = await scan_path_async(git_repos, Path(full_path))
|
227
|
+
except Exception:
|
228
|
+
non_git_scans = scan_path(git_repos, Path(full_path))
|
229
|
+
finally:
|
230
|
+
build_tree_from_paths(root, dir_cache, non_git_scans, max_depth)
|
231
|
+
json_tree = json.dumps(root, indent=2)
|
232
|
+
return [TextContent(type="text", text=json_tree)]
|
233
|
+
|
234
|
+
|
235
|
+
async def find_git_repo_async(cwd: str) -> list[Path]:
|
236
|
+
# ripgrep first then find
|
237
|
+
try:
|
238
|
+
cmd = r"rg --files --glob '**/.git/HEAD' --hidden | sed 's|/\.git/HEAD$|/.git|'"
|
239
|
+
proc = await asyncio.create_subprocess_shell(
|
240
|
+
cmd,
|
241
|
+
cwd=cwd,
|
242
|
+
stdout=subprocess.PIPE,
|
243
|
+
stderr=subprocess.PIPE,
|
244
|
+
)
|
245
|
+
stdout, stderr = await proc.communicate()
|
246
|
+
|
247
|
+
if proc.returncode not in [0, 1]: # 0 = success, 1 = some files not found (normal)
|
248
|
+
stderr_text = stderr.decode().strip()
|
249
|
+
if stderr_text: # If there's stderr content, it's likely a real error
|
250
|
+
raise Exception(f"Find command error: {stderr_text}")
|
251
|
+
|
252
|
+
git_dirs = stdout.decode().strip().splitlines()
|
253
|
+
repo_paths: list[Path] = []
|
254
|
+
for git_dir in git_dirs:
|
255
|
+
if git_dir: # Skip empty lines
|
256
|
+
# Convert to Path object and get parent (removes .git)
|
257
|
+
repo_relative_path = Path(git_dir).parent
|
258
|
+
repo_paths.append(repo_relative_path)
|
259
|
+
return repo_paths
|
260
|
+
|
261
|
+
except Exception:
|
277
262
|
pass
|
278
263
|
|
279
|
-
|
264
|
+
cmd = r"find . -name .git -type d ! -path '*/\.*/*'"
|
265
|
+
proc = await asyncio.create_subprocess_shell(
|
266
|
+
cmd,
|
267
|
+
cwd=cwd,
|
268
|
+
stdout=subprocess.PIPE,
|
269
|
+
stderr=subprocess.PIPE,
|
270
|
+
)
|
271
|
+
stdout, stderr = await proc.communicate()
|
272
|
+
|
273
|
+
if proc.returncode not in [0, 1]: # 0 = success, 1 = some files not found (normal)
|
274
|
+
stderr_text = stderr.decode().strip()
|
275
|
+
if stderr_text: # If there's stderr content, it's likely a real error
|
276
|
+
raise Exception(f"Find command error: {stderr_text}")
|
277
|
+
|
278
|
+
git_dirs = stdout.decode().strip().splitlines()
|
279
|
+
repo_paths: list[Path] = []
|
280
|
+
|
281
|
+
for git_dir in git_dirs:
|
282
|
+
if git_dir: # Skip empty lines
|
283
|
+
# Convert to Path object and get parent (removes .git)
|
284
|
+
repo_relative_path = Path(git_dir).parent
|
285
|
+
repo_paths.append(repo_relative_path)
|
286
|
+
return repo_paths
|
287
|
+
|
288
|
+
|
289
|
+
def find_git_repos_python(start_path: Path) -> list[Path]:
|
290
|
+
r"""
|
291
|
+
Python fallback for: find . -name .git -type d ! -path '*/\.*/*'
|
292
|
+
|
293
|
+
Finds all .git directories, excluding those inside hidden directories.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
start_path: Starting directory (defaults to current directory)
|
297
|
+
|
298
|
+
Returns:
|
299
|
+
List of Path objects pointing to .git directories
|
300
|
+
"""
|
301
|
+
git_dirs = []
|
302
|
+
start_str = str(start_path)
|
303
|
+
|
304
|
+
for root, dirs, _ in os.walk(start_str, followlinks=False):
|
305
|
+
# Remove hidden directories from traversal
|
306
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
307
|
+
|
308
|
+
# Check if current directory contains .git
|
309
|
+
if ".git" in dirs:
|
310
|
+
# Calculate relative path
|
311
|
+
rel_root = os.path.relpath(root, start_str)
|
312
|
+
if rel_root == ".":
|
313
|
+
git_path = ".git"
|
314
|
+
else:
|
315
|
+
git_path = rel_root + "/.git"
|
316
|
+
|
317
|
+
git_dirs.append(Path(git_path))
|
318
|
+
|
319
|
+
# Remove .git from further traversal (we don't need to go inside it)
|
320
|
+
dirs.remove(".git")
|
321
|
+
|
322
|
+
return git_dirs
|
323
|
+
|
324
|
+
|
325
|
+
async def git_ls(git_cwd: Path) -> list[Path]:
|
326
|
+
cmd = r"git ls-files"
|
327
|
+
proc = await asyncio.create_subprocess_shell(cmd, cwd=git_cwd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
328
|
+
stdout, stderr = await proc.communicate()
|
329
|
+
if proc.returncode != 0:
|
330
|
+
stderr_text = stderr.decode().strip()
|
331
|
+
raise Exception(f"Command error with status {proc.returncode}: {stderr_text}")
|
332
|
+
|
333
|
+
paths = stdout.decode().strip().splitlines()
|
334
|
+
paths = [Path(path) for path in paths if path]
|
335
|
+
return paths
|
336
|
+
|
337
|
+
|
338
|
+
def build_tree_from_paths(root: dict, dir_cache: dict, paths: list[Path], max_depth: int):
|
339
|
+
paths = [path for path in paths if len(path.parts) <= max_depth]
|
340
|
+
|
341
|
+
for path in paths:
|
342
|
+
parts = path.parts
|
343
|
+
current_path = ""
|
344
|
+
current = root
|
345
|
+
n = len(parts)
|
346
|
+
|
347
|
+
for i, part in enumerate(parts):
|
348
|
+
if i == n - 1:
|
349
|
+
current["children"].append({"name": part, "type": "file"})
|
350
|
+
else:
|
351
|
+
current_path = str(Path(current_path) / part) if current_path else part
|
352
|
+
|
353
|
+
if current_path not in dir_cache:
|
354
|
+
new_dir = {"name": part, "type": "directory", "children": []}
|
355
|
+
current["children"].append(new_dir)
|
356
|
+
dir_cache[current_path] = new_dir
|
357
|
+
|
358
|
+
current = dir_cache[current_path]
|
359
|
+
|
360
|
+
|
361
|
+
def scan_path(ignore_paths: list[Path], cwd: Path) -> list[Path]:
|
362
|
+
# ignore_paths relative to cwd
|
363
|
+
ignore_absolute = {(cwd / ignore_path).resolve() for ignore_path in ignore_paths}
|
364
|
+
files: list[Path] = []
|
365
|
+
for root, dirs, filenames in os.walk(cwd):
|
366
|
+
root_path = Path(root)
|
367
|
+
|
368
|
+
# Remove hidden directories from dirs list (modifies os.walk behavior)
|
369
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
370
|
+
|
371
|
+
# Remove ignored directories from dirs list
|
372
|
+
dirs[:] = [d for d in dirs if (root_path / d).resolve() not in ignore_absolute]
|
373
|
+
|
374
|
+
# Add non-hidden files
|
375
|
+
for filename in filenames:
|
376
|
+
if not filename.startswith("."):
|
377
|
+
file_path = root_path / filename
|
378
|
+
# Return path relative to cwd
|
379
|
+
files.append(file_path.relative_to(cwd))
|
380
|
+
|
381
|
+
return files
|
382
|
+
|
383
|
+
|
384
|
+
async def scan_path_async(ignore_paths: list[Path], cwd: Path) -> list[Path]:
|
385
|
+
# try ripgrep first, then find
|
280
386
|
try:
|
281
|
-
|
282
|
-
|
387
|
+
rgignore = " ".join(f"--glob '!{path}/**'" for path in ignore_paths)
|
388
|
+
rgcmd = rf"rg --files {rgignore} ."
|
389
|
+
|
390
|
+
proc = await asyncio.create_subprocess_shell(
|
391
|
+
rgcmd,
|
392
|
+
cwd=cwd,
|
393
|
+
stdout=asyncio.subprocess.PIPE,
|
394
|
+
stderr=asyncio.subprocess.PIPE,
|
395
|
+
)
|
283
396
|
|
284
|
-
|
285
|
-
json_tree = json.dumps(tree, indent=2)
|
397
|
+
stdout, stderr = await proc.communicate()
|
286
398
|
|
287
|
-
|
288
|
-
|
289
|
-
|
399
|
+
if proc.returncode in [0, 1]:
|
400
|
+
paths = []
|
401
|
+
for line in stdout.decode().strip().splitlines():
|
402
|
+
if line:
|
403
|
+
paths.append(Path(line))
|
404
|
+
return paths
|
405
|
+
except Exception:
|
406
|
+
pass
|
407
|
+
|
408
|
+
ignore_paths += [Path("backend")]
|
409
|
+
|
410
|
+
findignore = " ".join(f"-path './{path}' -prune -o" for path in ignore_paths)
|
411
|
+
findcmd = f"find . {findignore} -type f ! -path '*/.*/*' ! -name '.*' -print"
|
412
|
+
|
413
|
+
proc = await asyncio.create_subprocess_shell(
|
414
|
+
findcmd,
|
415
|
+
cwd=cwd,
|
416
|
+
stdout=subprocess.PIPE,
|
417
|
+
stderr=subprocess.PIPE,
|
418
|
+
)
|
419
|
+
stdout, stderr = await proc.communicate()
|
420
|
+
|
421
|
+
if proc.returncode not in [0, 1]: # 0 = success, 1 = some files not found (normal)
|
422
|
+
stderr_text = stderr.decode().strip()
|
423
|
+
if stderr_text: # If there's stderr content, it's likely a real error
|
424
|
+
raise Exception(f"Find command error: {stderr_text}")
|
425
|
+
|
426
|
+
paths = []
|
427
|
+
for line in stdout.decode().strip().splitlines():
|
428
|
+
if line:
|
429
|
+
if line.startswith("./"):
|
430
|
+
line = line[2:]
|
431
|
+
if line:
|
432
|
+
paths.append(Path(line))
|
433
|
+
|
434
|
+
return paths
|