54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
"""Install selected repository skills into Codex or another skill directory."""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def linked(path):
|
|
return path.is_symlink() or getattr(path, 'is_junction', lambda: False)()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('names', nargs='*', help='Skill folder names; defaults to handycontrol-wpf')
|
|
parser.add_argument('--all', action='store_true', help='Install all component skills')
|
|
parser.add_argument('--dest', type=Path, default=Path(os.environ.get('CODEX_HOME', str(Path.home() / '.codex'))) / 'skills')
|
|
parser.add_argument('--force', action='store_true', help='Replace files in existing skill folders')
|
|
parser.add_argument('--list', action='store_true')
|
|
args = parser.parse_args()
|
|
available = sorted(p.parent.name for p in SOURCE.glob('*/SKILL.md'))
|
|
if args.list:
|
|
print('\n'.join(available))
|
|
return
|
|
if args.all and args.names:
|
|
parser.error('Use names or --all, not both')
|
|
names = available if args.all else list(dict.fromkeys(args.names or ['handycontrol-wpf']))
|
|
destination = args.dest.expanduser().resolve()
|
|
# Preflight every selection before copying anything. No deletes, no shell execution.
|
|
for name in names:
|
|
if not re.fullmatch(r'handycontrol-wpf(?:-[a-z0-9]+)*', name) or name not in available:
|
|
parser.error(f'Unknown skill: {name}')
|
|
target = destination / name
|
|
if target.resolve().is_relative_to(SOURCE.resolve()) or SOURCE.resolve().is_relative_to(target.resolve()):
|
|
parser.error(f'Destination overlaps repository skills: {target}')
|
|
if linked(target):
|
|
parser.error(f'Destination must not be a link: {target}')
|
|
if target.exists() and not args.force:
|
|
parser.error(f'Already exists: {target}; use --force to update files')
|
|
if target.exists() and not target.is_dir():
|
|
parser.error(f'Not a directory: {target}')
|
|
# Refuse linked descendants too, including when updating an existing skill.
|
|
if target.exists() and any(linked(p) for p in target.rglob('*')):
|
|
parser.error(f'Destination contains links: {target}')
|
|
for name in names:
|
|
shutil.copytree(SOURCE / name, destination / name, dirs_exist_ok=args.force)
|
|
print(f'Installed {name} -> {destination / name}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|