#!/usr/bin/env python3 """ IDE Operator Rules Seeder Seeds operator rules into VS Code extension folders to provide policy-aware guidance for AI assistants and code generation. This script: 1. Finds VS Code extension directories 2. Copies/symlinks operator rules to the appropriate locations 3. Works across Mac, Linux, and Windows 4. Can watch for extension updates and auto-reseed 5. Verifies symlink integrity Usage: python seed_ide_rules.py # Auto-detect and seed python seed_ide_rules.py --list # List target directories python seed_ide_rules.py --symlink # Use symlinks instead of copy python seed_ide_rules.py --dry-run # Show what would be done python seed_ide_rules.py --watch # Watch for extension updates and auto-reseed python seed_ide_rules.py --verify # Verify all symlinks are intact """ from __future__ import annotations import argparse import os import platform import shutil import sys import time from pathlib import Path from typing import List, Optional, Set, Tuple # Source rules files to seed RULES_FILES = [ "IDE_OPERATOR_RULES.md", "AGENT_GUARDRAILS.md", ] # Target extension patterns and their rule directories EXTENSION_TARGETS = [ # Azure GitHub Copilot extension { "pattern": "ms-azuretools.vscode-azure-github-copilot-*", "subdir": "resources/azureRules", "target_name": "cloudflare.instructions.md", }, # GitHub Copilot extension (if it has a rules dir) { "pattern": "github.copilot-*", "subdir": "resources", "target_name": "operator.instructions.md", }, ] def get_vscode_extensions_dirs() -> List[Path]: """Get VS Code extension directories for the current platform.""" system = platform.system() home = Path.home() dirs: List[Path] = [] if system == "Darwin": # macOS dirs = [ home / ".vscode" / "extensions", home / ".vscode-insiders" / "extensions", home / ".cursor" / "extensions", # Cursor editor ] elif system == "Linux": dirs = [ home / ".vscode" / "extensions", home / ".vscode-server" / "extensions", # Remote SSH home / ".vscode-insiders" / "extensions", ] elif system == "Windows": dirs = [ home / ".vscode" / "extensions", home / ".vscode-insiders" / "extensions", Path(os.environ.get("APPDATA", "")) / "Code" / "User" / "extensions", ] return [d for d in dirs if d.exists()] def find_target_extensions(base_dirs: List[Path]) -> List[Tuple[Path, dict]]: """Find matching extension directories.""" targets: List[Tuple[Path, dict]] = [] for base_dir in base_dirs: for ext_config in EXTENSION_TARGETS: pattern = ext_config["pattern"] # Use glob to find matching extensions for ext_path in base_dir.glob(pattern): if ext_path.is_dir(): targets.append((ext_path, ext_config)) return targets def get_source_rules_path() -> Path: """Get the path to the source rules file.""" # Try relative to this script first script_dir = Path(__file__).parent.parent for rules_file in RULES_FILES: source = script_dir / rules_file if source.exists(): return source # Try current working directory for rules_file in RULES_FILES: source = Path.cwd() / rules_file if source.exists(): return source # Try parent of cwd (in case running from scripts/) for rules_file in RULES_FILES: source = Path.cwd().parent / rules_file if source.exists(): return source raise FileNotFoundError( f"Could not find any of {RULES_FILES}. " "Run this script from the CLOUDFLARE repo root." ) def seed_rules( source: Path, targets: List[Tuple[Path, dict]], use_symlink: bool = False, dry_run: bool = False, ) -> List[str]: """Seed rules to target directories.""" results: List[str] = [] for ext_path, config in targets: subdir = config["subdir"] target_name = config["target_name"] target_dir = ext_path / subdir target_file = target_dir / target_name # Create target directory if needed if not dry_run: target_dir.mkdir(parents=True, exist_ok=True) action = "symlink" if use_symlink else "copy" if dry_run: results.append(f"[DRY RUN] Would {action}: {source} → {target_file}") continue try: # Remove existing file/symlink if target_file.exists() or target_file.is_symlink(): target_file.unlink() if use_symlink: target_file.symlink_to(source.resolve()) results.append(f"✅ Symlinked: {target_file}") else: shutil.copy2(source, target_file) results.append(f"✅ Copied: {target_file}") except PermissionError: results.append(f"❌ Permission denied: {target_file}") except Exception as e: results.append(f"❌ Failed: {target_file} — {e}") return results def list_targets(targets: List[Tuple[Path, dict]]) -> None: """List all target directories.""" print("\n📁 Found VS Code extension targets:\n") if not targets: print(" No matching extensions found.") print(" Install ms-azuretools.vscode-azure-github-copilot to enable seeding.") return for ext_path, config in targets: print(f" 📦 {ext_path.name}") print(f" Path: {ext_path}") print(f" Target: {config['subdir']}/{config['target_name']}") print() def verify_symlinks( targets: List[Tuple[Path, dict]], source: Path, ) -> List[str]: """Verify all symlinks point to correct source.""" results: List[str] = [] for ext_path, config in targets: target_file = ext_path / config["subdir"] / config["target_name"] if target_file.is_symlink(): try: if target_file.resolve() == source.resolve(): results.append(f"✅ Valid: {config['target_name']} in {ext_path.name}") else: results.append( f"⚠️ Stale: {target_file.name} → {target_file.resolve()}" ) except OSError: results.append(f"💀 Broken symlink: {target_file}") elif target_file.exists(): results.append(f"📄 Copy (not symlink): {target_file.name} in {ext_path.name}") else: results.append(f"❌ Missing: {config['target_name']} in {ext_path.name}") return results def watch_and_reseed( source: Path, use_symlink: bool = True, interval: int = 60, ) -> None: """Watch for new extensions and auto-reseed.""" print(f"👁️ Watching for extension updates (every {interval}s)...") print(" Press Ctrl+C to stop\n") known_extensions: Set[str] = set() # Initial seed base_dirs = get_vscode_extensions_dirs() targets = find_target_extensions(base_dirs) known_extensions = {str(t[0]) for t in targets} results = seed_rules(source, targets, use_symlink=use_symlink) seeded = sum(1 for r in results if r.startswith("✅")) print(f"📊 Initial seed: {seeded}/{len(results)} targets") while True: try: time.sleep(interval) base_dirs = get_vscode_extensions_dirs() targets = find_target_extensions(base_dirs) current = {str(t[0]) for t in targets} new_extensions = current - known_extensions removed_extensions = known_extensions - current if new_extensions: print(f"\n🆕 {len(new_extensions)} new extension(s) detected") # Only seed new ones new_targets = [(p, c) for p, c in targets if str(p) in new_extensions] results = seed_rules(source, new_targets, use_symlink=use_symlink) for r in results: print(f" {r}") if removed_extensions: print(f"\n🗑️ {len(removed_extensions)} extension(s) removed") known_extensions = current except KeyboardInterrupt: print("\n\n👋 Stopped watching") break def main() -> int: parser = argparse.ArgumentParser( description="Seed IDE operator rules into VS Code extensions" ) parser.add_argument( "--list", "-l", action="store_true", help="List target extension directories", ) parser.add_argument( "--symlink", "-s", action="store_true", help="Use symlinks instead of copying files", ) parser.add_argument( "--dry-run", "-n", action="store_true", help="Show what would be done without making changes", ) parser.add_argument( "--watch", "-w", action="store_true", help="Watch for extension updates and auto-reseed (runs in foreground)", ) parser.add_argument( "--verify", "-v", action="store_true", help="Verify all symlinks are intact", ) parser.add_argument( "--interval", type=int, default=60, help="Watch interval in seconds (default: 60)", ) parser.add_argument( "--source", type=Path, help="Source rules file (default: auto-detect)", ) args = parser.parse_args() # Find VS Code extension directories base_dirs = get_vscode_extensions_dirs() if not base_dirs: print("❌ No VS Code extension directories found.") print(" Make sure VS Code is installed.") return 1 print(f"🔍 Searching in {len(base_dirs)} VS Code extension directories...") # Find target extensions targets = find_target_extensions(base_dirs) if args.list: list_targets(targets) return 0 if not targets: print("\n⚠️ No matching extensions found.") print(" Install one of these extensions to enable rule seeding:") print(" - ms-azuretools.vscode-azure-github-copilot") print(" - github.copilot") return 1 # Get source file try: source = args.source or get_source_rules_path() except FileNotFoundError as e: print(f"❌ {e}") return 1 # Handle --verify if args.verify: print(f"📄 Source: {source}") print(f"🔍 Verifying {len(targets)} target(s)...\n") results = verify_symlinks(targets, source) print("\n".join(results)) valid = sum(1 for r in results if r.startswith("✅")) stale = sum(1 for r in results if r.startswith("⚠️")) missing = sum(1 for r in results if r.startswith("❌")) broken = sum(1 for r in results if r.startswith("💀")) print(f"\n📊 {valid}/{len(results)} symlinks valid") if stale: print(f" ⚠️ {stale} stale (run --symlink to fix)") if missing: print(f" ❌ {missing} missing (run --symlink to create)") if broken: print(f" 💀 {broken} broken (run --symlink to fix)") return 0 if (missing == 0 and broken == 0) else 1 # Handle --watch if args.watch: print(f"📄 Source: {source}") watch_and_reseed(source, use_symlink=True, interval=args.interval) return 0 print(f"📄 Source: {source}") print(f"🎯 Found {len(targets)} target extension(s)") if args.dry_run: print("\n🔍 Dry run mode — no changes will be made\n") # Seed the rules results = seed_rules( source=source, targets=targets, use_symlink=args.symlink, dry_run=args.dry_run, ) print("\n" + "\n".join(results)) # Summary success = sum(1 for r in results if r.startswith("✅")) failed = sum(1 for r in results if r.startswith("❌")) if not args.dry_run: print(f"\n📊 Seeded {success}/{len(results)} targets") if failed: print(f" ⚠️ {failed} failed — check permissions") return 0 if failed == 0 else 1 if __name__ == "__main__": sys.exit(main())