Skip to content

Commit e10d769

Browse files
authored
Merge pull request #424 from dwash96/v0.96.7
v0.96.7
2 parents 925a30e + 50177b5 commit e10d769

38 files changed

+585
-354
lines changed

cecli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from packaging import version
22

3-
__version__ = "0.96.6.dev"
3+
__version__ = "0.96.7.dev"
44
safe_version = __version__
55

66
try:

cecli/args.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,7 @@ def get_parser(default_config_files, git_root):
957957
"-c",
958958
"--config",
959959
is_config_file=True,
960+
env_var="CECLI_CONFIG_FILE",
960961
metavar="CONFIG_FILE",
961962
help=(
962963
"Specify the config file (default: search for .cecli.conf.yml in git root, cwd"

cecli/commands/add.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from cecli.commands.utils.base_command import BaseCommand
77
from cecli.commands.utils.helpers import (
88
format_command_result,
9+
get_file_completions,
910
parse_quoted_filenames,
1011
quote_filename,
1112
)
@@ -82,10 +83,6 @@ async def execute(cls, io, coder, args, **kwargs):
8283
for matched_file in sorted(all_matched_files):
8384
abs_file_path = coder.abs_root_path(matched_file)
8485

85-
if not abs_file_path.startswith(coder.root) and not is_image_file(matched_file):
86-
io.tool_error(f"Can not add {abs_file_path}, which is not within {coder.root}")
87-
continue
88-
8986
if (
9087
coder.repo
9188
and coder.repo.git_ignored_file(matched_file)
@@ -205,10 +202,22 @@ def expand_subdir(file_path: Path) -> List[Path]:
205202
@classmethod
206203
def get_completions(cls, io, coder, args) -> List[str]:
207204
"""Get completion options for add command."""
208-
files = set(coder.get_all_relative_files())
209-
files = files - set(coder.get_inchat_relative_files())
210-
files = [quote_filename(fn) for fn in files]
211-
return files
205+
# Get both directory-based completions and filtered "all" completions
206+
directory_completions = get_file_completions(
207+
coder,
208+
args=args,
209+
completion_type="directory",
210+
include_directories=True,
211+
filter_in_chat=False,
212+
)
213+
214+
all_completions = get_file_completions(
215+
coder, args=args, completion_type="all", include_directories=False, filter_in_chat=True
216+
)
217+
218+
# Return the joint set (union) of both completion types
219+
joint_set = set(directory_completions) | set(all_completions)
220+
return sorted(joint_set)
212221

213222
@classmethod
214223
def get_help(cls) -> str:

cecli/commands/read_only.py

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from cecli.commands.utils.base_command import BaseCommand
88
from cecli.commands.utils.helpers import (
99
format_command_result,
10+
get_file_completions,
1011
parse_quoted_filenames,
1112
quote_filename,
1213
)
@@ -207,50 +208,22 @@ def _add_read_only_directory(
207208
@classmethod
208209
def get_completions(cls, io, coder, args) -> List[str]:
209210
"""Get completion options for read-only command."""
210-
from pathlib import Path
211-
212-
root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()
213-
214-
# Handle the prefix - could be partial path like "src/ma" or just "ma"
215-
if "/" in args:
216-
# Has directory component
217-
dir_part, file_part = args.rsplit("/", 1)
218-
if dir_part == "":
219-
search_dir = Path("/")
220-
path_prefix = "/"
221-
else:
222-
# Use os.path.expanduser for ~ support if needed, but Path handles it mostly
223-
search_dir = (root / dir_part).resolve()
224-
path_prefix = dir_part + "/"
225-
search_prefix = file_part.lower()
226-
else:
227-
search_dir = root
228-
search_prefix = args.lower()
229-
path_prefix = ""
230-
231-
completions = []
232-
try:
233-
if search_dir.exists() and search_dir.is_dir():
234-
for entry in search_dir.iterdir():
235-
name = entry.name
236-
if search_prefix and not name.lower().startswith(search_prefix):
237-
continue
238-
239-
# Add trailing slash for directories
240-
if entry.is_dir():
241-
completions.append(path_prefix + name + "/")
242-
else:
243-
completions.append(path_prefix + name)
244-
except (PermissionError, OSError):
245-
pass
211+
# Get both directory-based completions and filtered "all" completions
212+
directory_completions = get_file_completions(
213+
coder,
214+
args=args,
215+
completion_type="directory",
216+
include_directories=True,
217+
filter_in_chat=False,
218+
)
246219

247-
# Also include files already in the chat that match
248-
add_completions = coder.commands.get_completions("/add")
249-
for c in add_completions:
250-
if args.lower() in str(c).lower() and str(c) not in completions:
251-
completions.append(str(c))
220+
all_completions = get_file_completions(
221+
coder, args=args, completion_type="all", include_directories=False, filter_in_chat=True
222+
)
252223

253-
return sorted(completions)
224+
# Return the joint set (union) of both completion types
225+
joint_set = set(directory_completions) | set(all_completions)
226+
return sorted(joint_set)
254227

255228
@classmethod
256229
def get_help(cls) -> str:

cecli/commands/read_only_stub.py

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from cecli.commands.utils.base_command import BaseCommand
88
from cecli.commands.utils.helpers import (
99
format_command_result,
10+
get_file_completions,
1011
parse_quoted_filenames,
1112
quote_filename,
1213
)
@@ -207,49 +208,22 @@ def _add_read_only_directory(
207208
@classmethod
208209
def get_completions(cls, io, coder, args) -> List[str]:
209210
"""Get completion options for read-only-stub command."""
210-
from pathlib import Path
211-
212-
root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()
213-
214-
# Handle the prefix - could be partial path like "src/ma" or just "ma"
215-
if "/" in args:
216-
# Has directory component
217-
dir_part, file_part = args.rsplit("/", 1)
218-
if dir_part == "":
219-
search_dir = Path("/")
220-
path_prefix = "/"
221-
else:
222-
search_dir = (root / dir_part).resolve()
223-
path_prefix = dir_part + "/"
224-
search_prefix = file_part.lower()
225-
else:
226-
search_dir = root
227-
search_prefix = args.lower()
228-
path_prefix = ""
229-
230-
completions = []
231-
try:
232-
if search_dir.exists() and search_dir.is_dir():
233-
for entry in search_dir.iterdir():
234-
name = entry.name
235-
if search_prefix and not name.lower().startswith(search_prefix):
236-
continue
237-
238-
# Add trailing slash for directories
239-
if entry.is_dir():
240-
completions.append(path_prefix + name + "/")
241-
else:
242-
completions.append(path_prefix + name)
243-
except (PermissionError, OSError):
244-
pass
211+
# Get both directory-based completions and filtered "all" completions
212+
directory_completions = get_file_completions(
213+
coder,
214+
args=args,
215+
completion_type="directory",
216+
include_directories=True,
217+
filter_in_chat=False,
218+
)
245219

246-
# Also include files already in the chat that match
247-
add_completions = coder.commands.get_completions("/add")
248-
for c in add_completions:
249-
if args.lower() in str(c).lower() and str(c) not in completions:
250-
completions.append(str(c))
220+
all_completions = get_file_completions(
221+
coder, args=args, completion_type="all", include_directories=False, filter_in_chat=True
222+
)
251223

252-
return sorted(completions)
224+
# Return the joint set (union) of both completion types
225+
joint_set = set(directory_completions) | set(all_completions)
226+
return sorted(joint_set)
253227

254228
@classmethod
255229
def get_help(cls) -> str:

cecli/commands/utils/helpers.py

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,116 @@ def glob_filtered_to_repo(pattern: str, root: str, repo) -> List[Path]:
4646
else:
4747
try:
4848
raw_matched_files = list(Path(root).glob(pattern))
49-
except (IndexError, AttributeError):
49+
except (IndexError, AttributeError, ValueError):
5050
# Handle patterns like "**/*.py" that might fail on empty dirs
5151
raw_matched_files = []
5252

53-
# Filter out directories and ignored files
53+
# Expand directories and filter
5454
matched_files = []
5555
for f in raw_matched_files:
56-
if not f.is_file():
57-
continue
58-
if repo and repo.ignored_file(f):
59-
continue
60-
matched_files.append(f)
56+
matched_files.extend(expand_subdir(f))
57+
58+
# Filter to repository files
59+
matched_files = [fn.relative_to(root) for fn in matched_files if fn.is_relative_to(root)]
60+
61+
# if repo, filter against it
62+
if repo:
63+
git_files = repo.get_tracked_files()
64+
matched_files = [fn for fn in matched_files if str(fn) in git_files]
6165

6266
return matched_files
6367
except Exception as e:
6468
raise CommandError(f"Error processing pattern '{pattern}': {e}")
6569

6670

71+
def get_file_completions(
72+
coder,
73+
args: str = "",
74+
completion_type: str = "all",
75+
include_directories: bool = False,
76+
filter_in_chat: bool = False,
77+
) -> List[str]:
78+
"""
79+
Get file completions for command line arguments.
80+
81+
This function provides unified file completion logic that can be used by
82+
multiple commands (add, read-only, read-only-stub, etc.).
83+
84+
Args:
85+
coder: Coder instance
86+
args: Command arguments to complete
87+
completion_type: Type of completion to perform:
88+
- "all": Return all available files (default)
89+
- "glob": Treat args as glob pattern and expand
90+
- "directory": Perform directory-based prefix matching
91+
include_directories: Whether to include directories in results
92+
filter_in_chat: Whether to filter out files already in chat
93+
94+
Returns:
95+
List of completion strings (quoted if needed)
96+
"""
97+
from pathlib import Path
98+
99+
root = Path(coder.root) if hasattr(coder, "root") else Path.cwd()
100+
101+
if completion_type == "glob":
102+
# Handle glob pattern completion
103+
if not args.strip():
104+
return []
105+
106+
try:
107+
matched_files = glob_filtered_to_repo(args, str(root), coder.repo)
108+
completions = [str(fn) for fn in matched_files]
109+
except CommandError:
110+
completions = []
111+
112+
elif completion_type == "directory":
113+
# Handle directory-based prefix matching (like read-only commands)
114+
if "/" in args:
115+
# Has directory component
116+
dir_part, file_part = args.rsplit("/", 1)
117+
if dir_part == "":
118+
search_dir = Path("/")
119+
path_prefix = "/"
120+
else:
121+
search_dir = (root / dir_part).resolve()
122+
path_prefix = dir_part + "/"
123+
search_prefix = file_part.lower()
124+
else:
125+
search_dir = root
126+
search_prefix = args.lower()
127+
path_prefix = ""
128+
129+
completions = []
130+
try:
131+
if search_dir.exists() and search_dir.is_dir():
132+
for entry in search_dir.iterdir():
133+
name = entry.name
134+
if search_prefix and not name.lower().startswith(search_prefix):
135+
continue
136+
137+
# Add trailing slash for directories if requested
138+
if entry.is_dir() and include_directories:
139+
completions.append(path_prefix + name + "/")
140+
elif entry.is_file():
141+
completions.append(path_prefix + name)
142+
except (PermissionError, OSError):
143+
pass
144+
145+
else: # "all" completion type
146+
# Get all available files
147+
if filter_in_chat:
148+
files = set(coder.get_all_relative_files())
149+
files = files - set(coder.get_inchat_relative_files())
150+
completions = list(files)
151+
else:
152+
completions = coder.get_all_relative_files()
153+
154+
# Quote filenames with spaces
155+
completions = [quote_filename(fn) for fn in completions]
156+
return sorted(completions)
157+
158+
67159
def validate_file_access(io, coder, file_path: str, require_in_chat: bool = False) -> bool:
68160
"""
69161
Validate file access permissions and state.
@@ -130,13 +222,16 @@ def get_available_files(coder, in_chat: bool = False) -> List[str]:
130222
return coder.get_all_relative_files()
131223

132224

133-
def expand_subdir(file_path):
225+
def expand_subdir(file_path: Path) -> List[Path]:
134226
"""Expand a directory path to all files within it."""
135227
if file_path.is_file():
136-
yield file_path
137-
return
228+
return [file_path]
138229

139230
if file_path.is_dir():
231+
files = []
140232
for file in file_path.rglob("*"):
141233
if file.is_file():
142-
yield file
234+
files.append(file)
235+
return files
236+
237+
return []

cecli/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,9 @@ async def setup_git(git_root, io):
181181
)
182182
return
183183
elif cwd and await io.confirm_ask(
184-
"No git repo found, create one to track cecli's changes (recommended)?", acknowledge=True
184+
"No git repo found, create one to track cecli's changes (recommended)?",
185+
acknowledge=True,
186+
explicit_yes_required=True,
185187
):
186188
git_root = str(cwd.resolve())
187189
repo = await make_new_repo(git_root, io)

cecli/tools/command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class Tool(BaseTool):
3434
}
3535

3636
@classmethod
37-
async def execute(cls, coder, command_string, background=False, stop_background=None):
37+
async def execute(cls, coder, command_string, background=False, stop_background=None, **kwargs):
3838
"""
3939
Execute a shell command, optionally in background.
4040
"""

cecli/tools/command_interactive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Tool(BaseTool):
2626
}
2727

2828
@classmethod
29-
async def execute(cls, coder, command_string):
29+
async def execute(cls, coder, command_string, **kwargs):
3030
"""
3131
Execute an interactive shell command using run_cmd (which uses pexpect/PTY).
3232
"""

cecli/tools/context_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class Tool(BaseTool):
5151
}
5252

5353
@classmethod
54-
def execute(cls, coder, remove=None, editable=None, view=None, create=None):
54+
def execute(cls, coder, remove=None, editable=None, view=None, create=None, **kwargs):
5555
"""Perform batch operations on the coder's context.
5656
5757
Parameters

0 commit comments

Comments
 (0)