83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Remove the last exact instruction block added to a client overlay."""
|
|
|
|
import argparse
|
|
import os
|
|
import stat
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
def remove_instruction(path: Path, instruction: str) -> bool:
|
|
if not path.exists():
|
|
return False
|
|
if path.is_symlink() or not path.is_file():
|
|
raise ValueError(f'Instruction path is not a regular file: {path}')
|
|
|
|
raw = path.read_bytes()
|
|
encoding = 'utf-8-sig' if raw.startswith(b'\xef\xbb\xbf') else 'utf-8'
|
|
lines = raw.decode(encoding).splitlines(keepends=True)
|
|
wanted = instruction.rstrip('\r\n').splitlines()
|
|
values = [line.rstrip('\r\n') for line in lines]
|
|
offset = next((i for i in range(len(values) - len(wanted), -1, -1)
|
|
if values[i:i + len(wanted)] == wanted), None)
|
|
if offset is None:
|
|
return False
|
|
|
|
updated = ''.join(lines[:offset] + lines[offset + len(wanted):])
|
|
if not updated:
|
|
path.unlink()
|
|
return True
|
|
|
|
temporary = None
|
|
try:
|
|
with tempfile.NamedTemporaryFile(dir=path.parent, prefix='.instruction-', delete=False) as file:
|
|
temporary = Path(file.name)
|
|
file.write(updated.encode(encoding))
|
|
os.chmod(temporary, stat.S_IMODE(path.stat().st_mode))
|
|
os.replace(temporary, path)
|
|
finally:
|
|
if temporary is not None:
|
|
temporary.unlink(missing_ok=True)
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--client', choices=('codex', 'claude', 'both'), required=True)
|
|
parser.add_argument('--instruction', required=True)
|
|
parser.add_argument('--home', required=True)
|
|
args = parser.parse_args()
|
|
if not args.instruction.strip() or len(args.instruction) > 65536:
|
|
parser.error('Instruction must contain between 1 and 65536 characters.')
|
|
home = Path(args.home)
|
|
if not home.is_absolute():
|
|
parser.error('Home path must be absolute.')
|
|
directory = home / '.my-ai-configuration' / 'instructions'
|
|
if (home / '.my-ai-configuration').is_symlink() or directory.is_symlink():
|
|
parser.error('Instruction directory must not be a link.')
|
|
try:
|
|
inside_home = os.path.commonpath((home.resolve(), directory.resolve())) == str(home.resolve())
|
|
except ValueError:
|
|
inside_home = False
|
|
if not inside_home:
|
|
parser.error('Instruction directory must remain inside the home directory.')
|
|
|
|
targets = ('codex', 'claude') if args.client == 'both' else (args.client,)
|
|
paths = [(target, directory / f'{target}.md') for target in targets]
|
|
for _, path in paths:
|
|
if path.is_symlink():
|
|
parser.error('Instruction path must not be a link.')
|
|
if path.exists() and not path.is_file():
|
|
parser.error('Instruction path must be a regular file.')
|
|
for target, path in paths:
|
|
if remove_instruction(path, args.instruction):
|
|
print(f'Instruction removed for {target}.')
|
|
else:
|
|
print(f'Instruction not found for {target}.')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|