mirror of
https://github.com/paperclipai/agent-plugins.git
synced 2026-11-09 17:39:43 -10:00
73 lines
3.2 KiB
Python
Executable File
73 lines
3.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Install the canonical tree and host discovery links without overwriting user files."""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EXCLUDES = {'.git', 'node_modules', '__pycache__', '.DS_Store'}
|
|
|
|
|
|
def files(root):
|
|
return {p.relative_to(root): p for p in root.rglob('*')
|
|
if p.is_file() and not (set(p.relative_to(root).parts) & EXCLUDES)}
|
|
|
|
|
|
def install(runtime, scope, project=None, copy=False, home=None):
|
|
base = (Path(home or Path.home()) if scope == 'user' else Path(project)).resolve()
|
|
discovery = base / ('.claude' if runtime == 'claude' else '.agents')
|
|
bundle = base / '.agents' / 'poteto-stack' if copy else ROOT
|
|
if copy and (bundle == ROOT or ROOT in bundle.parents):
|
|
raise ValueError('Copy destination must be outside the source repository')
|
|
source_files = files(ROOT) if copy else {}
|
|
conflicts = []
|
|
if copy:
|
|
for rel, src in source_files.items():
|
|
dest = bundle / rel
|
|
if os.path.lexists(dest) and (dest.is_symlink() or not dest.is_file() or dest.read_bytes() != src.read_bytes()):
|
|
conflicts.append(str(dest))
|
|
links = [(discovery / 'skills' / p.name, bundle / 'skills' / p.name)
|
|
for p in sorted((ROOT / 'skills').iterdir()) if (p / 'SKILL.md').is_file()]
|
|
if runtime == 'claude':
|
|
links += [(discovery / 'agents' / p.name, bundle / 'agents' / p.name)
|
|
for p in sorted((ROOT / 'agents').glob('*.md'))]
|
|
for dest, target in links:
|
|
if os.path.lexists(dest) and not (dest.is_symlink() and dest.resolve() == target.resolve()):
|
|
conflicts.append(str(dest))
|
|
if conflicts:
|
|
raise ValueError('Conflicting files; review before installing:\n' + '\n'.join(conflicts))
|
|
if copy:
|
|
for rel, src in source_files.items():
|
|
dest = bundle / rel
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
if not dest.exists():
|
|
shutil.copy2(src, dest)
|
|
for dest, target in links:
|
|
if not os.path.lexists(dest):
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
dest.symlink_to(os.path.relpath(target, dest.parent), target_is_directory=target.is_dir())
|
|
return links
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--runtime', required=True, choices=['codex', 'claude'])
|
|
parser.add_argument('--scope', required=True, choices=['user', 'project'])
|
|
parser.add_argument('--project', type=Path)
|
|
parser.add_argument('--copy', action='store_true', help='Vendor files into .agents/poteto-stack; use for remote checkouts')
|
|
args = parser.parse_args()
|
|
if args.scope == 'project' and not args.project:
|
|
parser.error('--scope project requires --project')
|
|
try:
|
|
links = install(args.runtime, args.scope, args.project, args.copy)
|
|
except (ValueError, OSError) as error:
|
|
parser.exit(1, str(error) + '\n')
|
|
print(f'Installed {len(links)} discovery links for {args.runtime}. Existing unrelated files preserved.')
|
|
print('Start a new session and invoke poteto-mode. The installer does not change global instructions or schedule work.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|