{
"tool_response": {
"stdout": "#!\/usr\/bin\/env python3\n\"\"\"\nBackup CLI - Erstellt Artefakt-Versionen für Code-Dateien.\n\nSpeichert Backups als Versionen in masterdev.artefakt_versions.\n\nUsage:\n backup <file> [reason]\n backup \/var\/www\/freund\/shared\/database.py \"Vor Refactoring\"\n backup --list \/var\/www\/freund\/shared\/database.py\n backup --restore \/var\/www\/freund\/shared\/database.py --version 3\n\"\"\"\n\nimport argparse\nimport os\nimport sys\nfrom datetime import datetime\nfrom pathlib import Path\n\n# Add freund to path\nsys.path.insert(0, '\/var\/www\/freund')\n\nfrom shared.database import Database\nfrom shared.logger import LazyLogger\nfrom shared.file_ops import FileOps\n\nlog = LazyLogger(__name__)\n\n\nclass BackupCLI:\n \"\"\"Backup CLI for artefakt versioning.\"\"\"\n\n def __init__(self):\n self.db = Database('masterdev')\n\n def get_artefakt_by_path(self, file_path: str, auto_create: bool = True) -> dict:\n \"\"\"Get artefakt by file path (slug), optionally create if missing.\"\"\"\n from domains.code.modules.scan.repository import convert_code_slug\n\n # Normalize path and convert to datei~ slug\n file_path = os.path.abspath(file_path)\n slug = convert_code_slug(file_path)\n\n artefakt = self.db.query(\n \"SELECT id, slug, title, content FROM artefakte WHERE slug = %s\",\n [slug],\n fetch_one=True\n )\n\n if not artefakt:\n if auto_create and os.path.exists(file_path):\n artefakt = self._create_artefakt_for_file(file_path)\n else:\n raise ValueError(f\"Kein Artefakt für Pfad: {file_path}\")\n\n return artefakt\n\n def _create_artefakt_for_file(self, file_path: str) -> dict:\n \"\"\"Create artefakt entry for file.\"\"\"\n import hashlib\n from domains.code.modules.scan.repository import convert_code_slug\n\n content = FileOps.read(file_path, backup=False)\n title = os.path.basename(file_path)\n slug = convert_code_slug(file_path)\n content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()\n\n artefakt_id = self.db.insert('artefakte', {\n 'slug': slug,\n 'title': title,\n 'content': content,\n 'hash': content_hash,\n 'content_hash': content_hash,\n })\n\n log.info(f\"Artefakt erstellt für: {slug} (ID: {artefakt_id})\")\n\n return {\n 'id': artefakt_id,\n 'slug': slug,\n 'title': title,\n 'content': content,\n }\n\n def get_next_version(self, artefakt_id: int) -> int:\n \"\"\"Get next version number for artefakt.\"\"\"\n result = self.db.query(\n \"SELECT COALESCE(MAX(version), 0) + 1 as next_version FROM artefakt_versions WHERE artefakt_id = %s\",\n [artefakt_id],\n fetch_one=True\n )\n return result['next_version']\n\n def create_backup(self, file_path: str, reason: str = None) -> dict:\n \"\"\"\n Create backup of file as artefakt version.\n\n Args:\n file_path: Path to file\n reason: Backup reason\/summary\n\n Returns:\n Dict with backup info\n \"\"\"\n file_path = os.path.abspath(file_path)\n\n # Check file exists\n if not os.path.exists(file_path):\n raise FileNotFoundError(f\"Datei nicht gefunden: {file_path}\")\n\n # Get artefakt\n artefakt = self.get_artefakt_by_path(file_path)\n\n # Read current file content\n content = FileOps.read(file_path, backup=False)\n\n # Get next version\n version = self.get_next_version(artefakt['id'])\n\n # Create version entry\n self.db.execute(\n \"\"\"INSERT INTO artefakt_versions\n (artefakt_id, version, title, content, tags, changed_by, change_summary)\n VALUES (%s, %s, %s, %s, %s, %s, %s)\"\"\",\n [\n artefakt['id'],\n version,\n artefakt['title'],\n content,\n None, # Tags from current artefakt could be added\n 'backup-cli',\n reason or f\"Backup v{version}\"\n ]\n )\n\n log.info(f\"Backup erstellt: {file_path} -> Version {version}\")\n\n return {\n 'artefakt_id': artefakt['id'],\n 'version': version,\n 'file_path': file_path,\n 'reason': reason,\n 'created_at': datetime.now().isoformat()\n }\n\n def list_versions(self, file_path: str) -> list:\n \"\"\"List all versions of a file.\"\"\"\n file_path = os.path.abspath(file_path)\n artefakt = self.get_artefakt_by_path(file_path)\n\n versions = self.db.query(\n \"\"\"SELECT version, changed_by, change_summary, created_at,\n LENGTH(content) as size\n FROM artefakt_versions\n WHERE artefakt_id = %s\n ORDER BY version DESC\"\"\",\n [artefakt['id']]\n )\n\n return versions\n\n def restore_version(self, file_path: str, version: int) -> dict:\n \"\"\"\n Restore file to specific version.\n\n Args:\n file_path: Path to file\n version: Version number to restore\n\n Returns:\n Dict with restore info\n \"\"\"\n file_path = os.path.abspath(file_path)\n artefakt = self.get_artefakt_by_path(file_path)\n\n # Get version content\n ver = self.db.query(\n \"\"\"SELECT content, change_summary FROM artefakt_versions\n WHERE artefakt_id = %s AND version = %s\"\"\",\n [artefakt['id'], version],\n fetch_one=True\n )\n\n if not ver:\n raise ValueError(f\"Version {version} nicht gefunden\")\n\n # Create backup of current state first\n self.create_backup(file_path, f\"Auto-Backup vor Restore zu v{version}\")\n\n # Write restored content\n FileOps.write(file_path, ver['content'], backup=False)\n\n log.info(f\"Wiederhergestellt: {file_path} -> Version {version}\")\n\n return {\n 'file_path': file_path,\n 'restored_version': version,\n 'summary': ver['change_summary']\n }\n\n def diff_version(self, file_path: str, version: int) -> str:\n \"\"\"Show diff between current file and version.\"\"\"\n import difflib\n\n file_path = os.path.abspath(file_path)\n artefakt = self.get_artefakt_by_path(file_path)\n\n # Get version content\n ver = self.db.query(\n \"SELECT content FROM artefakt_versions WHERE artefakt_id = %s AND version = %s\",\n [artefakt['id'], version],\n fetch_one=True\n )\n\n if not ver:\n raise ValueError(f\"Version {version} nicht gefunden\")\n\n current = FileOps.read(file_path, backup=False)\n old = ver['content'] or ''\n\n diff = difflib.unified_diff(\n old.splitlines(keepends=True),\n current.splitlines(keepends=True),\n fromfile=f\"v{version}\",\n tofile=\"current\",\n lineterm=''\n )\n\n return ''.join(diff)\n\n def close(self):\n \"\"\"Close database connection.\"\"\"\n self.db.close()\n\n\ndef main():\n \"\"\"Main entry point using CLIFactory.\"\"\"\n from shared.cli_factory import CLIFactory\n\n backup_cli = BackupCLI()\n\n def cmd_create(args):\n \"\"\"Create backup of file.\"\"\"\n result = backup_cli.create_backup(args.file, getattr(args, 'reason', None))\n return result\n\n def cmd_list(args):\n \"\"\"List versions of file.\"\"\"\n return backup_cli.list_versions(args.file)\n\n def cmd_restore(args):\n \"\"\"Restore file to version.\"\"\"\n if not args.version:\n raise ValueError(\"--version erforderlich für restore\")\n return backup_cli.restore_version(args.file, args.version)\n\n def cmd_diff(args):\n \"\"\"Show diff to version.\"\"\"\n if not args.version:\n raise ValueError(\"--version erforderlich für diff\")\n diff = backup_cli.diff_version(args.file, args.version)\n if diff:\n print(diff)\n else:\n print(\"Keine Unterschiede\")\n return None\n\n cli = CLIFactory('backup', description='Backup CLI - Artefakt-Versionierung', version='1.0.0')\n\n cli.add_command('create', handler=cmd_create, help='Backup erstellen', args=[\n {'name': 'file', 'help': 'Datei-Pfad'},\n {'name': 'reason', 'nargs': '?', 'help': 'Backup-Grund'},\n ])\n\n cli.add_command('list', handler=cmd_list, help='Versionen auflisten', aliases=['ls'], args=[\n {'name': 'file', 'help': 'Datei-Pfad'},\n ])\n\n cli.add_command('restore', handler=cmd_restore, help='Version wiederherstellen', args=[\n {'name': 'file', 'help': 'Datei-Pfad'},\n {'name': '--version', 'short': '-n', 'type': 'int', 'required': True, 'help': 'Version'},\n ])\n\n cli.add_command('diff', handler=cmd_diff, help='Diff zu Version anzeigen', args=[\n {'name': 'file', 'help': 'Datei-Pfad'},\n {'name': '--version', 'short': '-n', 'type': 'int', 'required': True, 'help': 'Version'},\n ])\n\n try:\n return cli.run()\n finally:\n backup_cli.close()\n\n\nif __name__ == '__main__':\n sys.exit(main())",
"stderr": "",
"interrupted": false,
"isImage": false,
"noOutputExpected": false
}
}