migrate_antigravity.py Antigravity released a migration tool to automatically transfer extensions, settings, and conversation histories from Antigravity v1 to the new Antigravity IDE. The script handles process termination, file migration, and logging to ensure a smooth transition. | /usr/bin/env python3 | | | """ | | | Antigravity to Antigravity IDE Migration Tool | | | ============================================== | | | A self-contained script to automatically migrate extensions, custom settings, keybindings, | | | snippets, workspace states, and entire conversation histories from Antigravity v1 | | | to the new Antigravity IDE. | | | Author: Antigravity AI Coding Assistant pair-programmed with USER | | | License: MIT | | | """ | | | import os | | | import sys | | | import time | | | import json | | | import shutil | | | import sqlite3 | | | import base64 | | | import signal | | | import glob | | | import subprocess | | | Define base paths dynamically based on user's home folder | | | HOME = os.path.expanduser "~" | | | OLD DOT ANTIGRAVITY = os.path.join HOME, ".antigravity" | | | NEW DOT ANTIGRAVITY = os.path.join HOME, ".antigravity-ide" | | | OLD DOT GEMINI = os.path.join HOME, ".gemini/antigravity" | | | NEW DOT GEMINI = os.path.join HOME, ".gemini/antigravity-ide" | | | OLD SUPPORT = os.path.join HOME, "Library/Application Support/Antigravity" | | | NEW SUPPORT = os.path.join HOME, "Library/Application Support/Antigravity IDE" | | | LOG FILE = os.path.join NEW DOT GEMINI, "migration run.log" | | | def log msg, level="INFO" : | | | timestamp = time.strftime '%Y-%m-%d %H:%M:%S' | | | formatted msg = f" {timestamp} {level} {msg}" | | | print formatted msg | | | try: | | | Create directory if it doesn't exist | | | os.makedirs os.path.dirname LOG FILE , exist ok=True | | | with open LOG FILE, "a", encoding="utf-8" as f: | | | f.write formatted msg + "\n" | | | except Exception: | | | pass | | | def kill running processes : | | | log "Scanning and force-terminating any running Antigravity processes..." | | | my pid = os.getpid | | | killed any = False | | | 1. Broad pgrep/pkill attempts | | | try: | | | subprocess.run "pkill", "-f", "Antigravity IDE" , stderr=subprocess.DEVNULL | | | subprocess.run "pkill", "-f", "Antigravity" , stderr=subprocess.DEVNULL | | | except Exception: | | | pass | | | 2. Precise manual process scanning | | | try: | | | output = subprocess.check output "ps", "-ef" .decode 'utf-8', errors='ignore' | | | for line in output.splitlines : | | | if "Antigravity" in line or "Antigravity IDE" in line and "grep" not in line and "migrate antigravity.py" not in line: | | | parts = line.split | | | if len parts 1: | | | pid = int parts 1 | | | if pid = my pid: | | | try: | | | os.kill pid, signal.SIGKILL | | | log f"Terminated process {pid}: {' '.join parts 7: }", "PROCESS" | | | killed any = True | | | except Exception: | | | pass | | | except Exception as e: | | | log f"Error checking processes: {e}", "WARNING" | | | if killed any: | | | log "Successfully terminated active processes. Waiting 3 seconds for file locks to release..." | | | time.sleep 3 | | | else: | | | log "No active Antigravity or Antigravity IDE processes found." | | | def migrate extensions : | | | log "Migrating extensions..." | | | old ext dir = os.path.join OLD DOT ANTIGRAVITY, "extensions" | | | new ext dir = os.path.join NEW DOT ANTIGRAVITY, "extensions" | | | old json path = os.path.join old ext dir, "extensions.json" | | | new json path = os.path.join new ext dir, "extensions.json" | | | if not os.path.exists old json path : | | | log "Old extensions.json not found. Skipping extension migration.", "WARNING" | | | return | | | os.makedirs new ext dir, exist ok=True | | | if not os.path.exists new json path : | | | with open new json path, "w", encoding="utf-8" as f: | | | f.write " " | | | try: | | | with open old json path, "r", encoding="utf-8" as f: | | | old exts = json.load f | | | with open new json path, "r", encoding="utf-8" as f: | | | new exts = json.load f | | | except Exception as e: | | | log f"Failed to read extensions JSON files: {e}", "ERROR" | | | return | | | new exts dict = {ext.get "identifier", {} .get "id", "" .lower : ext for ext in new exts if ext.get "identifier" } | | | added count = 0 | | | copied dirs = 0 | | | for old ext in old exts: | | | identifier = old ext.get "identifier", {} | | | ext id = identifier.get "id", "" .lower | | | if not ext id: | | | continue | | | if ext id in new exts dict: | | | log f"Extension '{ext id}' is already installed in target. Skipping registration." | | | continue | | | Clone data and rewrite directory paths | | | migrated ext = json.loads json.dumps old ext | | | if "location" in migrated ext and "path" in migrated ext "location" : | | | migrated ext "location" "path" = migrated ext "location" "path" .replace OLD DOT ANTIGRAVITY, NEW DOT ANTIGRAVITY | | | if "location" in migrated ext and "fsPath" in migrated ext "location" : | | | migrated ext "location" "fsPath" = migrated ext "location" "fsPath" .replace OLD DOT ANTIGRAVITY, NEW DOT ANTIGRAVITY | | | if "location" in migrated ext and "external" in migrated ext "location" : | | | migrated ext "location" "external" = migrated ext "location" "external" .replace OLD DOT ANTIGRAVITY, NEW DOT ANTIGRAVITY | | | Copy directory if it exists | | | rel loc = old ext.get "relativeLocation" | | | if rel loc: | | | src dir = os.path.join old ext dir, rel loc | | | dst dir = os.path.join new ext dir, rel loc | | | if os.path.exists src dir : | | | if not os.path.exists dst dir : | | | log f"Copying extension directory: {rel loc}" | | | try: | | | shutil.copytree src dir, dst dir, symlinks=True | | | copied dirs += 1 | | | except Exception as e: | | | log f"Failed to copy directory {rel loc}: {e}", "ERROR" | | | else: | | | log f"Extension directory '{rel loc}' already exists in target." | | | else: | | | log f"Listed extension directory not found at: {src dir}", "WARNING" | | | new exts.append migrated ext | | | added count += 1 | | | try: | | | with open new json path, "w", encoding="utf-8" as f: | | | json.dump new exts, f, indent=None | | | log f"Successfully migrated {added count} extension entries and copied {copied dirs} folders." | | | except Exception as e: | | | log f"Failed to write merged extensions.json: {e}", "ERROR" | | | def merge vscdb src db, dst db, merge protobufs=False : | | | log f"Merging database: {os.path.basename src db }" | | | if not os.path.exists src db : | | | return | | | os.makedirs os.path.dirname dst db , exist ok=True | | | 1. Read destination target keys | | | dst records = {} | | | if os.path.exists dst db : | | | try: | | | conn = sqlite3.connect dst db | | | cursor = conn.cursor | | | cursor.execute "SELECT key, value FROM ItemTable" | | | for row in cursor.fetchall : | | | dst records row 0 = row 1 | | | conn.close | | | except Exception as e: | | | log f"Failed to read existing keys from target DB: {e}", "WARNING" | | | 2. Backup target DB | | | if os.path.exists dst db : | | | backup path = dst db + ".migration backup" | | | try: | | | shutil.copy2 dst db, backup path | | | except Exception as e: | | | log f"Failed to backup target DB: {e}", "WARNING" | | | 3. Read source old keys | | | src records = {} | | | try: | | | conn = sqlite3.connect src db | | | cursor = conn.cursor | | | cursor.execute "SELECT key, value FROM ItemTable" | | | for row in cursor.fetchall : | | | src records row 0 = row 1 | | | conn.close | | | except Exception as e: | | | log f"Failed to read keys from source DB: {e}", "ERROR" | | | return | | | 4. Merge records | | | merged records = src records.copy | | | if merge protobufs: | | | Special base64 protobuf concatenation merge for conversation histories | | | protobuf keys = | | | "antigravityUnifiedStateSync.trajectorySummaries", | | | "antigravityUnifiedStateSync.sidebarWorkspaces" | | | | | | for key in dst records: | | | if key in protobuf keys and key in src records: | | | try: | | | src bytes = base64.b64decode src records key | | | dst bytes = base64.b64decode dst records key | | | Protobuf repeated fields can be merged by simple binary concatenation | | | merged bytes = src bytes + dst bytes | | | merged val = base64.b64encode merged bytes .decode 'utf-8' | | | merged records key = merged val | | | log f"Concatenated protobuf state key: {key} {len src bytes }B source + {len dst bytes }B target " | | | except Exception as e: | | | log f"Failed to merge protobuf key '{key}': {e}. Overwriting with target value.", "WARNING" | | | merged records key = dst records key | | | else: | | | merged records key = dst records key | | | else: | | | Standard merge: let target current session keys override source keys on conflict | | | merged records.update dst records | | | 5. Write back to destination database | | | try: | | | Copy schema/DB first to preserve indices/SQLite page configurations | | | if os.path.exists src db : | | | shutil.copy2 src db, dst db | | | conn = sqlite3.connect dst db | | | cursor = conn.cursor | | | cursor.execute "CREATE TABLE IF NOT EXISTS ItemTable key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB " | | | cursor.execute "DELETE FROM ItemTable" | | | for key, val in merged records.items : | | | cursor.execute "INSERT OR REPLACE INTO ItemTable key, value VALUES ?, ? ", key, val | | | conn.commit | | | conn.close | | | log f"Wrote {len merged records } keys to database successfully." | | | except Exception as e: | | | log f"Failed to write merged database: {e}", "ERROR" | | | def merge json files src path, dst path : | | | if not os.path.exists src path : | | | return | | | os.makedirs os.path.dirname dst path , exist ok=True | | | dst data = {} | | | if os.path.exists dst path : | | | try: | | | with open dst path, "r", encoding="utf-8" as f: | | | dst data = json.load f | | | except Exception: | | | pass | | | try: | | | with open src path, "r", encoding="utf-8" as f: | | | src data = json.load f | | | except Exception as e: | | | log f"Failed to read source JSON {src path}: {e}", "ERROR" | | | return | | | merged data = src data.copy | | | merged data.update dst data Target overrides source on conflict | | | try: | | | with open dst path, "w", encoding="utf-8" as f: | | | json.dump merged data, f, indent=2 | | | except Exception as e: | | | log f"Failed to save merged JSON {dst path}: {e}", "ERROR" | | | def migrate app support : | | | log "Migrating User settings, keybindings, snippets, and workspace states..." | | | old user dir = os.path.join OLD SUPPORT, "User" | | | new user dir = os.path.join NEW SUPPORT, "User" | | | 1. Merge settings.json and copy keybindings.json | | | merge json files os.path.join old user dir, "settings.json" , os.path.join new user dir, "settings.json" | | | src kb = os.path.join old user dir, "keybindings.json" | | | dst kb = os.path.join new user dir, "keybindings.json" | | | if os.path.exists src kb : | | | shutil.copy2 src kb, dst kb | | | 2. Copy custom user snippets | | | src snippets = os.path.join old user dir, "snippets" | | | dst snippets = os.path.join new user dir, "snippets" | | | if os.path.exists src snippets and not os.path.exists dst snippets : | | | try: | | | shutil.copytree src snippets, dst snippets | | | except Exception as e: | | | log f"Failed to copy snippets: {e}", "WARNING" | | | 3. Copy workspaces and shared proto db directories | | | for folder in "Workspaces", "shared proto db" : | | | src f = os.path.join OLD SUPPORT, folder | | | dst f = os.path.join NEW SUPPORT, folder | | | if os.path.exists src f and not os.path.exists dst f : | | | try: | | | shutil.copytree src f, dst f | | | log f"Copied root storage folder: {folder}" | | | except Exception as e: | | | log f"Failed to copy folder {folder}: {e}", "WARNING" | | | 4. Migrate globalStorage databases with protobuf merge for trajectory/sidebar keys | | | old gs = os.path.join old user dir, "globalStorage" | | | new gs = os.path.join new user dir, "globalStorage" | | | if os.path.exists old gs : | | | os.makedirs new gs, exist ok=True | | | for item in os.listdir old gs : | | | src item = os.path.join old gs, item | | | dst item = os.path.join new gs, item | | | if item == "state.vscdb": | | | merge vscdb src item, dst item, merge protobufs=True | | | elif item == "state.vscdb.backup": | | | shutil.copy2 src item, dst item | | | elif item == "storage.json": | | | merge json files src item, dst item | | | else: | | | if os.path.isdir src item and not os.path.exists dst item : | | | shutil.copytree src item, dst item | | | elif os.path.isfile src item and not os.path.exists dst item : | | | shutil.copy2 src item, dst item | | | 5. Migrate workspaceStorage databases individual folders | | | old ws = os.path.join old user dir, "workspaceStorage" | | | new ws = os.path.join new user dir, "workspaceStorage" | | | if os.path.exists old ws : | | | os.makedirs new ws, exist ok=True | | | for workspace id in os.listdir old ws : | | | src w = os.path.join old ws, workspace id | | | dst w = os.path.join new ws, workspace id | | | if not os.path.isdir src w : | | | continue | | | if not os.path.exists dst w : | | | try: | | | shutil.copytree src w, dst w | | | except Exception as e: | | | log f"Failed to copy workspace storage {workspace id}: {e}", "WARNING" | | | else: | | | Target exists: Merge the inner state database | | | merge vscdb os.path.join src w, "state.vscdb" , os.path.join dst w, "state.vscdb" | | | src backup = os.path.join src w, "state.vscdb.backup" | | | dst backup = os.path.join dst w, "state.vscdb.backup" | | | if os.path.exists src backup : | | | shutil.copy2 src backup, dst backup | | | Merge missing items | | | for file item in os.listdir src w : | | | if file item not in "state.vscdb", "state.vscdb.backup" : | | | src f = os.path.join src w, file item | | | dst f = os.path.join dst w, file item | | | if os.path.isdir src f and not os.path.exists dst f : | | | shutil.copytree src f, dst f | | | elif os.path.isfile src f and not os.path.exists dst f : | | | shutil.copy2 src f, dst f | | | def migrate gemini configs : | | | log "Migrating Gemini assistant states & conversation logs..." | | | os.makedirs NEW DOT GEMINI, exist ok=True | | | 1. Copy antigravity state.pbtxt | | | src state = os.path.join OLD DOT GEMINI, "antigravity state.pbtxt" | | | dst state = os.path.join NEW DOT GEMINI, "antigravity state.pbtxt" | | | if os.path.exists src state : | | | shutil.copy2 src state, dst state | | | 2. Copy agyhub summaries proto.pb | | | src summary = os.path.join OLD DOT GEMINI, "agyhub summaries proto.pb" | | | dst summary = os.path.join NEW DOT GEMINI, "agyhub summaries proto.pb" | | | if os.path.exists src summary : | | | shutil.copy2 src summary, dst summary | | | 3. Setup MCP config symlink | | | target mcp = os.path.join NEW DOT GEMINI, "mcp config.json" | | | shared mcp = os.path.join HOME, ".gemini/config/mcp config.json" | | | if os.path.exists target mcp and not os.path.islink target mcp : | | | if os.path.getsize target mcp == 0: | | | try: | | | os.remove target mcp | | | os.symlink shared mcp, target mcp | | | log "Replaced empty target mcp config.json with symlink to shared config." | | | except Exception as e: | | | log f"Failed to symlink mcp config: {e}", "WARNING" | | | elif not os.path.exists target mcp and os.path.exists shared mcp : | | | try: | | | os.symlink shared mcp, target mcp | | | log "Created symlink for shared MCP configuration." | | | except Exception as e: | | | log f"Failed to create symlink for mcp config: {e}", "WARNING" | | | 4. Copy raw conversation protobuf .pb log files | | | src convo dir = os.path.join OLD DOT GEMINI, "conversations" | | | dst convo dir = os.path.join NEW DOT GEMINI, "conversations" | | | if os.path.exists src convo dir : | | | os.makedirs dst convo dir, exist ok=True | | | pb copied = 0 | | | for f in os.listdir src convo dir : | | | if f.endswith ".pb" : | | | src file = os.path.join src convo dir, f | | | dst file = os.path.join dst convo dir, f | | | if not os.path.exists dst file : | | | try: | | | shutil.copy2 src file, dst file | | | pb copied += 1 | | | except Exception as e: | | | log f"Failed to copy convo file {f}: {e}", "WARNING" | | | log f"Copied {pb copied} raw conversation protobuf logs .pb ." | | | def configure pbtxt states status : | | | log f"Setting pbtxt migration states to {status}..." | | | state files = | | | os.path.join OLD DOT GEMINI, "antigravity state.pbtxt" , | | | os.path.join NEW DOT GEMINI, "antigravity state.pbtxt" | | | | | | for state file in state files: | | | if os.path.exists state file : | | | try: | | | with open state file, "r", encoding="utf-8" as f: | | | content = f.read | | | if status == "UNSPECIFIED": | | | content = content.replace "migrate convos into projects: MIGRATION STATUS COMPLETED", | | | "migrate convos into projects: MIGRATION STATUS UNSPECIFIED" | | | else: | | | content = content.replace "migrate convos into projects: MIGRATION STATUS UNSPECIFIED", | | | "migrate convos into projects: MIGRATION STATUS COMPLETED" | | | with open state file, "w", encoding="utf-8" as f: | | | f.write content | | | log f"Successfully configured state file: {os.path.basename state file }" | | | except Exception as e: | | | log f"Failed to configure state file {state file}: {e}", "ERROR" | | | def run language server migration : | | | log "Locating language server binary inside Antigravity IDE app bundle..." | | | bin pattern = "/Applications/Antigravity IDE.app/Contents/Resources/app/extensions/antigravity/bin/language server macos " | | | binaries = glob.glob bin pattern | | | if not binaries: | | | log "No matching language server binary found Please make sure Antigravity IDE is installed in /Applications.", "ERROR" | | | return False | | | ls binary = binaries 0 | | | log f"Using language server binary: {ls binary}" | | | log "Spawning standalone language server to process conversation migrations..." | | | try: | | | proc = subprocess.Popen | | | ls binary, | | | "--standalone", | | | "--app data dir", "antigravity-ide" | | | , stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE | | | log f"Standalone language server successfully spawned PID: {proc.pid} . Running migration pass..." | | | time.sleep 8 Allow the Go server sufficient time to run migrations offline | | | proc.terminate | | | proc.wait timeout=2 | | | log "Standalone language server completed processing and shut down cleanly." | | | return True | | | except subprocess.TimeoutExpired: | | | try: | | | proc.kill | | | except Exception: | | | pass | | | log "Language server process took too long to exit and was forced terminated.", "WARNING" | | | return True | | | except Exception as e: | | | log f"Failed to execute standalone conversation migration: {e}", "ERROR" | | | return False | | | def count converted chats : | | | dst convo dir = os.path.join NEW DOT GEMINI, "conversations" | | | if not os.path.exists dst convo dir : | | | return 0 | | | try: | | | db files = f for f in os.listdir dst convo dir if f.endswith ".db" and len f 36 | | | return len db files | | | except Exception: | | | return 0 | | | def reopen ide : | | | log "Reopening Antigravity IDE..." | | | try: | | | subprocess.Popen "open", "/Applications/Antigravity IDE.app" | | | log "Open command issued successfully. IDE is launching." | | | except Exception as e: | | | log f"Failed to automatically restart IDE: {e}. You may start it manually.", "WARNING" | | | def main : | | | print r""" | | | ============================================================ | | | ANTIGRAVITY TO ANTIGRAVITY IDE MIGRATION TOOL | | | ============================================================ | | | This script safely restores your extensions, custom settings, | | | keybindings, snippets, custom workspaces, and conversation | | | history into the new Antigravity IDE. | | | ============================================================ | | | """ | | | Pre-checks | | | if not os.path.exists OLD DOT GEMINI : | | | print f" - Error: Could not locate old Antigravity config directory at {OLD DOT GEMINI}" | | | print " Make sure Antigravity v1 was installed on this machine." | | | sys.exit 1 | | | response = input " ? Proceed with offline migration? This will force quit Antigravity y/N : " | | | if response.strip .lower not in "y", "yes" : | | | print " - Migration cancelled by user." | | | sys.exit 0 | | | print "\n 1/7 Terminating running processes..." | | | kill running processes | | | print "\n 2/7 Migrating extensions..." | | | migrate extensions | | | print "\n 3/7 Migrating User settings, keybindings and layout states..." | | | migrate app support | | | print "\n 4/7 Copying logs and configuration metadata..." | | | migrate gemini configs | | | print "\n 5/7 Triggering Go-based conversation migration module..." | | | configure pbtxt states "UNSPECIFIED" | | | ls success = run language server migration | | | Restore completed state | | | configure pbtxt states "COMPLETED" | | | print "\n 6/7 Verifying migrated conversation databases..." | | | converted count = count converted chats | | | if converted count 0: | | | log f"Successfully converted {converted count} conversation history database s ", "SUCCESS" | | | else: | | | if ls success: | | | log "No conversations found, or the IDE will process them upon restart.", "INFO" | | | else: | | | log "Conversation database conversion was not completed. They will merge on next IDE launch.", "WARNING" | | | print "\n 7/7 Launching fresh Antigravity IDE instance..." | | | reopen ide | | | print f""" | | | ============================================================ | | | MIGRATION COMPLETE | | | ============================================================ | | | All items successfully merged into Antigravity IDE. | | | Logs are saved to: {LOG FILE} | | | You're ready to pick up exactly where you left off | | | ============================================================ | | | """ | | | if name == " main ": | | | main |