fix(reset): clear document entries while preserving scaffolds (#580)

* fix(reset): clear document entries while preserving scaffolds

* fix(reset): preserve scaffolds across Bash versions

* docs(reset): keep document inventory before presentation heading
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-18 07:33:41 +02:00
1 parent 3b16f80595
commit d590aa1b10
4 files changed
+170 -20

No files matched your search

+26 -11
View File
@@ -86,7 +86,11 @@ cv/main_example.tex. This scope covers skill files only.
### If scope includes `documents`: ### If scope includes `documents`:
Use Glob to list all files present in `documents/cv/`, `documents/linkedin/`, `documents/diplomas/`, `documents/references/`, `documents/projects/`, `documents/postings/`, and `documents/applications/`. Present as: Use Glob to list all files present in `documents/cv/`, `documents/linkedin/`, `documents/diplomas/`, `documents/references/`, `documents/projects/`, `documents/postings/`, and `documents/applications/`.
Include nested and hidden user entries in the inventory. Each subfolder's tracked `.gitkeep` is a scaffold placeholder: exclude it from the deletion list and from the empty/non-empty decision, and preserve it.
Present as:
``` ```
## Documents reset will delete: ## Documents reset will delete:
@@ -251,16 +255,27 @@ Leave the rest of the file intact: the portal-CLI and WebSearch-fallback explana
### Documents reset ### Documents reset
For each non-empty document subfolder, delete all files within it using Bash `rm`. Do not delete the folder itself, and do not delete `documents/README.md`. For each non-empty document subfolder, delete all user entries, including nested folders and hidden files. Preserve the seven top-level subfolders, their `.gitkeep` placeholders, and `documents/README.md`. Run the following Bash block only after the confirmation above; its glob settings stay inside the subshell. If `documents/` or any of its seven roots is a symbolic link, stop before deleting anything and ask the user to replace that linked root with a local document folder.
```bash ```bash
rm -f documents/cv/* (
rm -f documents/linkedin/* set -e
rm -f documents/diplomas/* for directory in documents documents/{cv,linkedin,diplomas,references,projects,postings,applications}; do
rm -f documents/references/* if [ -L "$directory" ]; then
rm -f documents/projects/* printf 'Reset stopped: %s is a symbolic link.\n' "$directory" >&2
rm -f documents/postings/* exit 1
rm -rf documents/applications/*/ fi
done
unset GLOBIGNORE
shopt -s dotglob nullglob
for directory in documents/cv documents/linkedin documents/diplomas documents/references documents/projects documents/postings documents/applications; do
for entry in "$directory"/*; do
if [ "${entry##*/}" != ".gitkeep" ]; then
rm -rf -- "$entry"
fi
done
done
)
``` ```
--- ---
@@ -287,7 +302,7 @@ Then tell the user what to do next based on what was reset:
> Note that `CLAUDE.md` and `cv/main_example.tex` are outside the `profile` scope and still hold your personal data. If you are handing this fork over or making it public, clear them by hand. > Note that `CLAUDE.md` and `cv/main_example.tex` are outside the `profile` scope and still hold your personal data. If you are handing this fork over or making it public, clear them by hand.
**If documents were reset:** **If documents were reset:**
> The `documents/` folder is now empty. Add your career documents and run `/setup` to populate your profile. See `documents/README.md` for instructions on what to put where. > Your user documents are now cleared. The folder structure, `.gitkeep` placeholders, and `documents/README.md` are preserved. Add your career documents and run `/setup` to populate your profile.
**If both were reset:** **If both were reset:**
> Both your profile files and documents folder are now empty. Add documents to `documents/` (or skip and use the CV import / interview path), then run `/setup`. > Your profile data and user documents are now cleared; the document folders, `.gitkeep` placeholders, and `documents/README.md` are preserved. Add documents to `documents/` (or skip and use the CV import / interview path), then run `/setup`.
+2
View File
@@ -68,6 +68,8 @@ per-file diff commands.
### Fixed ### Fixed
- **`/reset documents` clears nested and hidden user entries while preserving scaffolds** - cleanup retains tracked `.gitkeep` files and the README, with the complete inventory shown before confirmation (#580).
- **`jobindex-search` examples follow the search CLI contract** - the posted-today example includes a required query, unsupported flags are removed, and regression checks accept LF and CRLF checkouts (#579). - **`jobindex-search` examples follow the search CLI contract** - the posted-today example includes a required query, unsupported flags are removed, and regression checks accept LF and CRLF checkouts (#579).
- **`convert_salary_excel.py` converts the IDA workbook again** (#615, `tools/convert_salary_excel.py`, - **`convert_salary_excel.py` converts the IDA workbook again** (#615, `tools/convert_salary_excel.py`,
+22 -9
View File
@@ -28,13 +28,16 @@ tree, the profile files from /setup Step 3's own headings - so a new drop
folder or a new /setup target fails this test until /reset covers it. folder or a new /setup target fails this test until /reset covers it.
""" """
import re import re
import shutil
import subprocess import subprocess
import unittest import unittest
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory
REPO = Path(__file__).resolve().parent.parent REPO = Path(__file__).resolve().parent.parent
RESET = REPO / ".claude" / "commands" / "reset.md" RESET = REPO / ".claude" / "commands" / "reset.md"
SETUP = REPO / ".claude" / "commands" / "setup.md" SETUP = REPO / ".claude" / "commands" / "setup.md"
BASH = shutil.which("bash")
def tracked_document_subfolders(): def tracked_document_subfolders():
@@ -73,16 +76,26 @@ class TestResetCoversEveryDocumentsSubfolder(unittest.TestCase):
f"so the user confirms a deletion list that omits them: {missing}", f"so the user confirms a deletion list that omits them: {missing}",
) )
@unittest.skipUnless(BASH, "requires Bash to replay the documented /reset command")
def test_delete_block_removes_every_subfolder(self): def test_delete_block_removes_every_subfolder(self):
deleted = set(re.findall(r"rm -r?f documents/(\w+)/", self.text)) # Exercise the deletion contract, rather than requiring one particular
missing = sorted(self.folders - deleted) # spelling of rm. Keep deriving the folder set from git so a newly
self.assertEqual( # tracked document root still has to be handled by the real command.
missing, section = self.text.split("### Documents reset", 1)[1]
[], script = re.search(r"```bash\n(.*?)```", section, re.S).group(1)
"reset.md's delete block has no rm line for these documents/ " with TemporaryDirectory() as directory:
'subfolders, yet the command then claims "The `documents/` ' root = Path(directory)
f'folder is now empty.": {missing}', for folder in self.folders:
) path = root / "documents" / folder
path.mkdir(parents=True)
(path / ".gitkeep").write_text("Keep scaffold.\n")
(path / "user.txt").write_text("Remove document.\n")
result = subprocess.run([BASH, "-c", script], cwd=root, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
for folder in self.folders:
path = root / "documents" / folder
self.assertEqual([entry.name for entry in path.iterdir()], [".gitkeep"])
self.assertEqual((path / ".gitkeep").read_text(), "Keep scaffold.\n")
def section(text: str, start: str, end: str) -> str: def section(text: str, start: str, end: str) -> str:
+120
View File
@@ -0,0 +1,120 @@
"""Replay /reset's actual Bash commands against isolated document files."""
import errno
import os
import re
import shutil
import subprocess
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
COMMAND = Path(__file__).resolve().parent.parent / ".claude/commands/reset.md"
FOLDERS = ("cv", "linkedin", "diplomas", "references", "projects", "postings", "applications")
BASH = shutil.which("bash")
def symlinks_available():
"""Probe only the filesystem capability needed by the scope controls."""
with TemporaryDirectory() as directory:
root = Path(directory)
target = root / "target"
target.touch()
try:
(root / "link").symlink_to(target)
except NotImplementedError:
return False
except OSError as exc:
if exc.errno in (errno.ENOSYS, errno.ENOTSUP, errno.EPERM):
return False
if os.name == "nt" and getattr(exc, "winerror", None) in (5, 1314):
return False
raise
return True
SYMLINKS = bool(BASH) and symlinks_available()
@unittest.skipUnless(BASH, "requires Bash to replay the documented /reset command")
class ResetDocuments(unittest.TestCase):
def reset_script(self):
section = COMMAND.read_text(encoding="utf-8").split("### Documents reset", 1)[1]
return re.search(r"```bash\n(.*?)```", section, re.S).group(1)
def run_reset(self, populated):
with TemporaryDirectory() as directory:
root = Path(directory)
documents = root / "documents"
documents.mkdir()
readme = documents / "README.md"
readme.write_text("Keep these framework instructions.\n")
sibling = root / "outside-documents.md"
sibling.write_text("Keep this external file.\n")
for name in FOLDERS:
folder = documents / name
folder.mkdir()
(folder / ".gitkeep").write_text("Keep this scaffold.\n")
if populated:
(folder / "summary.md").write_text("A document\n")
(folder / ".source.md").write_text("A hidden document\n")
nested = folder / "project-or-application"
nested.mkdir()
(nested / "README.md").write_text("A nested document\n")
result = subprocess.run([BASH, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, "")
self.assertEqual(readme.read_text(), "Keep these framework instructions.\n")
self.assertEqual(sibling.read_text(), "Keep this external file.\n")
self.assertEqual({path.name for path in documents.iterdir()}, {*FOLDERS, "README.md"})
for name in FOLDERS:
self.assertEqual([path.name for path in (documents / name).iterdir()], [".gitkeep"])
self.assertEqual((documents / name / ".gitkeep").read_text(), "Keep this scaffold.\n")
def test_all_document_entries_are_removed_without_removing_the_scaffold(self):
self.run_reset(populated=True)
def test_empty_document_folders_remain_usable(self):
self.run_reset(populated=False)
@unittest.skipUnless(SYMLINKS, "requires filesystem symlinks and Windows symlink privilege")
def test_symlinked_roots_stop_before_any_deletion(self):
for linked_root in ("documents", *(f"documents/{name}" for name in FOLDERS)):
with self.subTest(linked_root=linked_root), TemporaryDirectory() as directory:
root = Path(directory)
outside = root / "outside"
outside.mkdir()
sentinel = outside / "keep.md"
sentinel.write_text("External document.\n")
documents = root / "documents"
documents.mkdir()
for name in FOLDERS:
folder = documents / name
folder.mkdir()
(folder / "local.md").write_text("Local document.\n")
target = root / linked_root
shutil.rmtree(target)
target.symlink_to(outside, target_is_directory=True)
locals_before = [path for name in FOLDERS for path in (documents / name).glob("local.md")]
result = subprocess.run([BASH, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("symbolic link", result.stderr)
self.assertEqual(sentinel.read_text(), "External document.\n")
for path in locals_before:
self.assertEqual(path.read_text(), "Local document.\n")
@unittest.skipUnless(SYMLINKS, "requires filesystem symlinks and Windows symlink privilege")
def test_internal_symlink_is_removed_without_following_its_target(self):
with TemporaryDirectory() as directory:
root = Path(directory)
outside = root / "outside"
outside.mkdir()
sentinel = outside / "keep.md"
sentinel.write_text("External document.\n")
for name in FOLDERS:
(root / "documents" / name).mkdir(parents=True)
link = root / "documents/projects/external"
link.symlink_to(outside, target_is_directory=True)
result = subprocess.run([BASH, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(link.is_symlink())
self.assertEqual(sentinel.read_text(), "External document.\n")