Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions opc/scripts/setup/claude_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,10 +812,11 @@ def find_latest_backup(claude_dir: Path) -> Path | None:

# Files to preserve during uninstall (user data accumulated since install)
PRESERVE_FILES = [
"history.jsonl", # Command history
"mcp_config.json", # MCP server configs
".env", # API keys and settings
"projects.json", # Project configs
"history.jsonl", # Command history
"mcp_config.json", # MCP server configs
".env", # API keys and settings
"projects.json", # Project configs
"settings.local.json", # User's custom permissions and overrides
]

PRESERVE_DIRS = [
Expand Down Expand Up @@ -918,8 +919,8 @@ def uninstall_opc_integration(
else:
shutil.copy2(archived_src, dest)
result["preserved"].append(name)
except Exception:
pass # Best effort
except Exception as e:
result.setdefault("preserve_warnings", []).append(f"{name}: {e}")

# Build summary message
msg_parts = ["Uninstalled successfully."]
Expand All @@ -930,6 +931,9 @@ def uninstall_opc_integration(
msg_parts.append(" No backup found (created empty .claude)")
if result["preserved"]:
msg_parts.append(f" Preserved user data: {', '.join(result['preserved'])}")
if result.get("preserve_warnings"):
for warning in result["preserve_warnings"]:
msg_parts.append(f" [WARN] Could not preserve {warning}")

result["message"] = "\n".join(msg_parts)
result["success"] = True
Expand Down
35 changes: 35 additions & 0 deletions opc/scripts/setup/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,41 @@ async def run_uninstall_wizard() -> None:
console.print(f"\n[green]SUCCESS[/green]\n{result['message']}")
else:
console.print(f"\n[red]FAILED[/red]\n{result['message']}")
return

# Additional cleanup: shell config
console.print("\n[bold]Additional cleanup:[/bold]")

# Remove CLAUDE_OPC_DIR from shell config
for shell_config in [Path.home() / ".zshrc", Path.home() / ".bashrc"]:
if shell_config.exists():
content = shell_config.read_text()
if "CLAUDE_OPC_DIR" in content:
lines = content.splitlines()
cleaned = [
line for line in lines
if "CLAUDE_OPC_DIR" not in line
and "Continuous-Claude OPC directory" not in line
]
shell_config.write_text("\n".join(cleaned) + "\n")
console.print(f" [green]OK[/green] Removed CLAUDE_OPC_DIR from {shell_config.name}")

# Offer to remove TLDR CLI
if shutil.which("tldr"):
if Confirm.ask("\n Remove TLDR CLI tool?", default=False):
import subprocess

remove_result = subprocess.run(
["uv", "tool", "uninstall", "llm-tldr"],
capture_output=True,
text=True,
timeout=30,
)

This comment was marked as outdated.

if remove_result.returncode == 0:
console.print(" [green]OK[/green] TLDR removed")
else:
console.print(" [yellow]WARN[/yellow] Could not remove TLDR")
console.print(" Remove manually: uv tool uninstall llm-tldr")
Comment on lines +1352 to +1370
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handling for subprocess.runTimeoutExpired and OSError can propagate uncaught.

If uv is not on PATH or the process times out (Line 1360 sets timeout=30), the resulting FileNotFoundError / subprocess.TimeoutExpired will bubble up unhandled and crash the wizard after an otherwise successful uninstall.

🛡️ Proposed fix: wrap in try/except
     if shutil.which("tldr"):
         if Confirm.ask("\n  Remove TLDR CLI tool?", default=False):
             import subprocess
 
-            remove_result = subprocess.run(
-                ["uv", "tool", "uninstall", "llm-tldr"],
-                capture_output=True,
-                text=True,
-                timeout=30,
-            )
-            if remove_result.returncode == 0:
-                console.print("  [green]OK[/green] TLDR removed")
-            else:
-                console.print("  [yellow]WARN[/yellow] Could not remove TLDR")
+            try:
+                remove_result = subprocess.run(
+                    ["uv", "tool", "uninstall", "llm-tldr"],
+                    capture_output=True,
+                    text=True,
+                    timeout=30,
+                )
+                if remove_result.returncode == 0:
+                    console.print("  [green]OK[/green] TLDR removed")
+                else:
+                    console.print("  [yellow]WARN[/yellow] Could not remove TLDR")
+                    console.print("  Remove manually: uv tool uninstall llm-tldr")
+            except (subprocess.TimeoutExpired, OSError) as e:
+                console.print(f"  [yellow]WARN[/yellow] Could not remove TLDR: {e}")
                 console.print("  Remove manually: uv tool uninstall llm-tldr")
🤖 Prompt for AI Agents
In `@opc/scripts/setup/wizard.py` around lines 1352 - 1366, The uninstall call
using subprocess.run inside the TLDR removal block should be wrapped in a
try/except to handle subprocess.TimeoutExpired and OSError/FileNotFoundError so
the wizard doesn't crash; locate the block that checks shutil.which("tldr") and
Confirm.ask, then around the subprocess.run call (which sets timeout=30) catch
subprocess.TimeoutExpired and OSError/FileNotFoundError (or the base OSError)
and log a warning via console.print including guidance to run "uv tool uninstall
llm-tldr" manually, and also handle non-zero return codes as currently done
using remove_result.returncode.



async def main():
Expand Down