fix(init): split --auto-mine from --yes; show file-count estimate before mine prompt
Reviewer feedback on the previous commit flagged two real problems:
1. Overloading --yes to also auto-mine was a silent behaviour change for
scripted callers. Today --yes only auto-accepts entities — making it
ALSO trigger a multi-minute ChromaDB write breaks every script that
currently runs `mempalace init --yes <dir>` for the fast non-interactive
entity path. Add a separate `--auto-mine` flag instead. Combinations:
mempalace init --yes <dir> # entities auto, STILL prompt mine
mempalace init --auto-mine <dir> # prompt entities, skip mine prompt
mempalace init --yes --auto-mine <dir> # fully non-interactive
--yes behaviour is now identical to pre-PR.
2. The mine prompt was firing without telling the user how big the job
was. On a real corpus mine takes minutes-to-tens-of-minutes; hitting
Enter on default-Y with no size cue is a footgun. Show a one-line
estimate computed from scan_project (the same walk we hand into mine)
BEFORE the prompt:
~423 files (~12 MB) would be mined into this palace.
Mine this directory now? [Y/n]
The estimate uses a single corpus walk: scan_project's output is
passed into mine() via a new optional files= kwarg, so we never walk
the tree twice.
Tests: replaced the old "--yes auto-mines" assertion with a regression
guard that --yes alone STILL prompts; added coverage for --auto-mine
alone, --yes --auto-mine together, and the pre-prompt estimate line.
This commit is contained in:
+69
-18
@@ -159,44 +159,83 @@ def cmd_init(args):
|
||||
# Pass 4: offer to run mine immediately. The directory just had its
|
||||
# rooms + entities set up, so 99% of users will mine next anyway —
|
||||
# asking here removes the "remember to type the next command" friction.
|
||||
# `--yes` skips the prompt and auto-mines (non-interactive path).
|
||||
# `--auto-mine` skips the prompt and mines automatically; `--yes` is
|
||||
# SCOPED to entity auto-accept and does NOT imply mining.
|
||||
_maybe_run_mine_after_init(args, cfg)
|
||||
|
||||
|
||||
def _format_size_mb(num_bytes: int) -> str:
|
||||
"""Render a byte count as a human-readable size for the mine estimate.
|
||||
|
||||
< 1 MB rounds up to ``<1 MB`` so users never see a misleading ``0 MB``
|
||||
on small projects. Otherwise reports an integer megabyte count.
|
||||
"""
|
||||
if num_bytes <= 0:
|
||||
return "<1 MB"
|
||||
mb = num_bytes / (1024 * 1024)
|
||||
if mb < 1:
|
||||
return "<1 MB"
|
||||
return f"{mb:.0f} MB"
|
||||
|
||||
|
||||
def _maybe_run_mine_after_init(args, cfg) -> None:
|
||||
"""Prompt the user to mine the directory just initialised, or auto-mine
|
||||
when ``--yes`` was passed. Extracted so the prompt path is unit-testable.
|
||||
when ``--auto-mine`` was passed. Extracted so the prompt path is
|
||||
unit-testable.
|
||||
|
||||
Behaviour matrix:
|
||||
|
||||
- default (no flags) — prompt, default Yes, mine in-process if accepted
|
||||
- ``--yes`` — entity auto-accept only; STILL prompts for the mine step
|
||||
- ``--auto-mine`` — skip the mine prompt and mine directly
|
||||
- ``--yes --auto-mine`` — fully non-interactive
|
||||
|
||||
Mine errors are surfaced (not swallowed): a failing mine exits with a
|
||||
non-zero status via :func:`sys.exit` so downstream scripts can see it.
|
||||
The pre-scan that produces the file-count estimate is reused as the
|
||||
mine input so we never walk the corpus twice.
|
||||
"""
|
||||
from .miner import mine, scan_project
|
||||
|
||||
project_dir = args.dir
|
||||
auto = bool(getattr(args, "yes", False))
|
||||
auto_mine = bool(getattr(args, "auto_mine", False))
|
||||
|
||||
# Pre-scan so the user knows roughly what they're agreeing to before
|
||||
# the prompt. The scan is fast and we'd run it inside mine() anyway.
|
||||
# Single corpus walk: this scan feeds BOTH the "what would be mined"
|
||||
# estimate the user sees in the prompt AND the file list mine() will
|
||||
# process. We pass the result into mine() via the `files` kwarg so it
|
||||
# doesn't re-walk the tree.
|
||||
try:
|
||||
files = scan_project(project_dir)
|
||||
file_count = len(files)
|
||||
scanned_files = scan_project(project_dir)
|
||||
file_count = len(scanned_files)
|
||||
total_bytes = 0
|
||||
for fp in scanned_files:
|
||||
try:
|
||||
total_bytes += fp.stat().st_size
|
||||
except OSError:
|
||||
# Skip files that vanished between scan and stat — mine()
|
||||
# will skip them too.
|
||||
continue
|
||||
size_str = _format_size_mb(total_bytes)
|
||||
except Exception:
|
||||
scanned_files = None
|
||||
file_count = None
|
||||
size_str = None
|
||||
|
||||
if auto:
|
||||
print("\n Auto-mining (--yes).")
|
||||
else:
|
||||
scope = (
|
||||
f" (~{file_count} files in scope)"
|
||||
if isinstance(file_count, int) and file_count > 0
|
||||
else ""
|
||||
)
|
||||
print(f"\n Ready to mine{scope}.")
|
||||
# Show the scope estimate BEFORE the prompt so the user knows what
|
||||
# they are agreeing to. On a real corpus mine takes minutes; hitting
|
||||
# Enter on a default-Y prompt with no size cue is a footgun.
|
||||
if isinstance(file_count, int):
|
||||
if size_str:
|
||||
print(f" ~{file_count} files (~{size_str}) would be mined into this palace.\n")
|
||||
else:
|
||||
print(f" ~{file_count} files would be mined into this palace.\n")
|
||||
|
||||
if not auto_mine:
|
||||
try:
|
||||
answer = input(" Mine this directory now? [Y/n] ").strip().lower()
|
||||
except EOFError:
|
||||
# Non-interactive stdin (e.g. piped) — treat like decline so
|
||||
# we don't block. User can re-run with --yes to opt in.
|
||||
# we don't block. User can re-run with --auto-mine to opt in.
|
||||
answer = "n"
|
||||
if answer not in ("", "y", "yes"):
|
||||
print(f"\n Skipped. Run `mempalace mine {project_dir}` when ready.")
|
||||
@@ -204,7 +243,11 @@ def _maybe_run_mine_after_init(args, cfg) -> None:
|
||||
|
||||
palace_path = cfg.palace_path
|
||||
try:
|
||||
mine(project_dir=project_dir, palace_path=palace_path)
|
||||
mine(
|
||||
project_dir=project_dir,
|
||||
palace_path=palace_path,
|
||||
files=scanned_files,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
# mine() handles its own SIGINT summary + sys.exit(130); re-raise
|
||||
# any KeyboardInterrupt that escapes (shouldn't happen) so the
|
||||
@@ -643,6 +686,14 @@ def main():
|
||||
action="store_true",
|
||||
help="Auto-accept all detected entities (non-interactive)",
|
||||
)
|
||||
p_init.add_argument(
|
||||
"--auto-mine",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Skip the post-init mine prompt and run mine automatically. "
|
||||
"Combine with --yes for a fully non-interactive setup."
|
||||
),
|
||||
)
|
||||
p_init.add_argument(
|
||||
"--lang",
|
||||
default=None,
|
||||
|
||||
+15
-6
@@ -981,8 +981,16 @@ def mine(
|
||||
dry_run: bool = False,
|
||||
respect_gitignore: bool = True,
|
||||
include_ignored: list = None,
|
||||
files: list = None,
|
||||
):
|
||||
"""Mine a project directory into the palace."""
|
||||
"""Mine a project directory into the palace.
|
||||
|
||||
``files`` may optionally be a pre-scanned list of file paths from
|
||||
:func:`scan_project`. When provided, the corpus walk is skipped — the
|
||||
caller (e.g. ``init`` showing a file-count estimate before the mine
|
||||
prompt) avoids walking the tree twice. When ``None`` (the default),
|
||||
``mine`` walks the tree itself just like before.
|
||||
"""
|
||||
|
||||
project_path = Path(project_dir).expanduser().resolve()
|
||||
config = load_config(project_dir)
|
||||
@@ -990,11 +998,12 @@ def mine(
|
||||
wing = wing_override or config["wing"]
|
||||
rooms = config.get("rooms", [{"name": "general", "description": "All project files"}])
|
||||
|
||||
files = scan_project(
|
||||
project_dir,
|
||||
respect_gitignore=respect_gitignore,
|
||||
include_ignored=include_ignored,
|
||||
)
|
||||
if files is None:
|
||||
files = scan_project(
|
||||
project_dir,
|
||||
respect_gitignore=respect_gitignore,
|
||||
include_ignored=include_ignored,
|
||||
)
|
||||
if limit > 0:
|
||||
files = files[:limit]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user