Protokoll #33020

ID33020
Zeitstempel2026-02-19 08:47:58.664124
Clientroot
IP209.198.153.163
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens2,486 (Input: 46, Output: 2,440)
Dauer343 ms
Request-Zeit2026-02-19 08:47:58.664124
Response-Zeit2026-02-19 08:47:59.007300

Request

{
    "event": "PreToolUse",
    "tool_name": "Bash",
    "tool_input": {
        "command": "ssh root@prod.karlkratz.com \"cat \/usr\/local\/bin\/backup\" 2>\/dev\/null",
        "description": "Read prod backup script"
    }
}

Response

{
    "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
    }
}
← Vorheriger Zur Liste Nächster →