#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = [] # /// """ Enhanced Contextune Configuration Interactive configuration management using AskUserQuestion tool. Features: - Dual-scope output style installation (user-level or project-level) - Status line integration - Installation manifest tracking - Clean uninstallation with warnings """ import json import shutil from pathlib import Path import sys import os import re # Add lib to path for manifest import sys.path.insert(0, str(Path(__file__).parent.parent / 'lib')) from install_manifest import read_manifest, update_output_style, update_status_line, clear_manifest def detect_state() -> dict: """ Detect current installation state. Returns: dict with complete state information """ # Check manifest first manifest = read_manifest() # Verify files still exist user_path = Path.home() / ".claude" / "output-styles" / "extraction-optimized.md" project_path = Path.cwd() / ".claude" / "output-styles" / "extraction-optimized.md" statusline_path = Path.home() / ".claude" / "statusline.sh" output_style_installed = False output_style_scope = None output_style_path = None if user_path.exists(): output_style_installed = True output_style_scope = 'user' output_style_path = str(user_path) elif project_path.exists(): output_style_installed = True output_style_scope = 'project' output_style_path = str(project_path) # Check status line status_line_installed = False if statusline_path.exists(): try: content = statusline_path.read_text() if 'Contextune' in content or 'ctx:' in content: status_line_installed = True except IOError: pass return { 'output_style': { 'installed': output_style_installed, 'scope': output_style_scope, 'path': output_style_path }, 'status_line': { 'installed': status_line_installed, 'path': str(statusline_path) if statusline_path.exists() else None }, 'manifest': manifest } def install_output_style(scope: str = 'user') -> tuple[bool, str]: """ Install extraction-optimized output style. Args: scope: 'user' for ~/.claude/output-styles/ or 'project' for .claude/output-styles/ Returns: (success: bool, installed_path: str) """ try: # Find plugin root via CLAUDE_PLUGIN_ROOT env var plugin_root = os.environ.get('CLAUDE_PLUGIN_ROOT') if not plugin_root: plugin_root = Path(__file__).parent.parent else: plugin_root = Path(plugin_root) source = plugin_root / "output-styles" / "extraction-optimized.md" if not source.exists(): print(f"❌ Source not found: {source}", file=sys.stderr) return False, "" # Determine destination based on scope if scope == 'user': dest_dir = Path.home() / ".claude" / "output-styles" else: # project dest_dir = Path.cwd() / ".claude" / "output-styles" dest_dir.mkdir(parents=True, exist_ok=True) dest = dest_dir / "extraction-optimized.md" # Copy file shutil.copy(source, dest) # Update manifest update_output_style(scope, str(dest)) return True, str(dest) except Exception as e: print(f"❌ Installation failed: {e}", file=sys.stderr) import traceback traceback.print_exc(file=sys.stderr) return False, "" def install_status_line() -> bool: """ Add Contextune section to ~/.claude/statusline.sh. Returns: bool indicating success """ try: statusline_path = Path.home() / ".claude" / "statusline.sh" # Create statusline.sh from template if doesn't exist if not statusline_path.exists(): statusline_path.parent.mkdir(parents=True, exist_ok=True) # Basic template template = '''#!/bin/bash # Claude Code Status Line OUTPUT="" # Section: Contextune Commands if grep -q '"contextune.*true' ~/.claude/settings.json 2>/dev/null; then YELLOW="\\033[1;33m" RESET="\\033[0m" OUTPUT="${OUTPUT}${YELLOW}Contextune:${RESET} /ctx:research | /ctx:plan | /ctx:execute" fi echo -e "$OUTPUT" ''' statusline_path.write_text(template) statusline_path.chmod(0o755) # Update manifest update_status_line(True, str(statusline_path)) return True # Read existing file content = statusline_path.read_text() # Check if Contextune already present if 'Contextune' in content or 'ctx:' in content: print("ℹ️ Contextune already in status line", file=sys.stderr) update_status_line(True, str(statusline_path)) return True # Find the final echo line lines = content.split('\n') insert_index = -1 for i in range(len(lines) - 1, -1, -1): if lines[i].strip().startswith('echo'): insert_index = i break if insert_index == -1: # No echo found, append at end insert_index = len(lines) # Create Contextune section contextune_section = [ '', '# Section: Contextune Commands', 'if grep -q \'"contextune.*true\' ~/.claude/settings.json 2>/dev/null; then', ' YELLOW="\\033[1;33m"', ' RESET="\\033[0m"', ' OUTPUT="${OUTPUT} | ${YELLOW}Contextune:${RESET} /ctx:research | /ctx:plan | /ctx:execute"', 'fi', '' ] # Insert section before echo new_lines = lines[:insert_index] + contextune_section + lines[insert_index:] # Write back statusline_path.write_text('\n'.join(new_lines)) # Update manifest update_status_line(True, str(statusline_path)) return True except Exception as e: print(f"❌ Status line installation failed: {e}", file=sys.stderr) import traceback traceback.print_exc(file=sys.stderr) return False def uninstall_output_style(manifest: dict) -> tuple[bool, int]: """ Remove output style based on manifest. Args: manifest: Installation manifest Returns: (success: bool, files_removed: int) """ try: removed = 0 output_style = manifest.get('output_style', {}) if output_style.get('installed'): path = output_style.get('path') if path and Path(path).exists(): Path(path).unlink() removed += 1 print(f"✅ Removed output style: {path}", file=sys.stderr) return True, removed except Exception as e: print(f"❌ Failed to remove output style: {e}", file=sys.stderr) return False, 0 def uninstall_status_line(manifest: dict) -> tuple[bool, bool]: """ Remove Contextune section from status line. Args: manifest: Installation manifest Returns: (success: bool, removed: bool) """ try: status_line = manifest.get('status_line', {}) if not status_line.get('installed'): return True, False statusline_path = Path.home() / ".claude" / "statusline.sh" if not statusline_path.exists(): return True, False # Read content content = statusline_path.read_text() # Remove Contextune section (from # Section: Contextune to fi) pattern = r'\n# Section: Contextune Commands\n.*?fi\n' new_content = re.sub(pattern, '', content, flags=re.DOTALL) if new_content != content: statusline_path.write_text(new_content) print(f"✅ Removed Contextune from status line", file=sys.stderr) return True, True return True, False except Exception as e: print(f"❌ Failed to remove status line section: {e}", file=sys.stderr) return False, False def cleanup_plans_directories() -> int: """ Find and remove .plans/ directories. Returns: Number of directories removed """ try: search_paths = [ Path.cwd(), Path.home() / "DevProjects", Path.home() / "Projects", Path.home() / "Code", Path.home() / "dev" ] plans_dirs = [] for search_path in search_paths: if search_path.exists() and search_path.is_dir(): for plans_dir in search_path.glob('**/.plans'): # Limit depth relative = plans_dir.relative_to(search_path) if plans_dir.is_relative_to(search_path) else plans_dir if len(relative.parts) <= 4: plans_dirs.append(plans_dir) removed = 0 for plans_dir in plans_dirs: try: shutil.rmtree(plans_dir) removed += 1 print(f" Removed: {plans_dir}", file=sys.stderr) except Exception as e: print(f" Failed to remove {plans_dir}: {e}", file=sys.stderr) return removed except Exception as e: print(f"❌ Cleanup failed: {e}", file=sys.stderr) return 0 def output_instructions_for_claude(): """ Output JSON instructions for Claude to use AskUserQuestion. """ state = detect_state() instructions = { 'state': state, 'next_action': 'use_ask_user_question', 'instructions': None } if not state['output_style']['installed']: # Not installed - offer to install instructions['instructions'] = { 'action': 'prompt_install', 'message': ( 'Output style not installed. Use AskUserQuestion tool:\n\n' 'Question: "Would you like to install the extraction-optimized output style?"\n' 'Header: "Setup"\n' 'Options:\n' '1. Install (Enable automatic documentation extraction)\n' '2. Skip (Can install later with /ctx:configure)\n\n' 'If Install selected: Ask about scope (next prompt)\n' 'If Skip: Show how to run /ctx:configure later' ), 'scope_prompt': ( 'Question: "Where should the output style be installed?"\n' 'Header: "Scope"\n' 'Options:\n' '1. This project - Install to .claude/output-styles/ (project-specific, git-trackable)\n' '2. All projects - Install to ~/.claude/output-styles/ (available everywhere)\n\n' 'After scope selected: Ask about status line (next prompt)' ), 'status_line_prompt': ( 'Question: "Would you like to add Contextune to your status bar?"\n' 'Header: "Status Bar"\n' 'Options:\n' '1. Yes (Show Contextune commands in status bar - zero tokens)\n' '2. No (Skip status bar integration)\n\n' 'After selection: Execute installation with chosen options' ) } else: # Already installed - offer management scope_text = "user-level" if state['output_style']['scope'] == 'user' else "project-level" instructions['instructions'] = { 'action': 'prompt_manage', 'current_state': { 'output_style': f"Installed ({scope_text})", 'status_line': "Installed" if state['status_line']['installed'] else "Not installed" }, 'message': ( f'Current installation:\n' f'• Output style: {state["output_style"]["scope"]}-level\n' f'• Status line: {"✅" if state["status_line"]["installed"] else "❌"}\n\n' 'Use AskUserQuestion tool:\n\n' 'Question: "Manage Contextune configuration"\n' 'Header: "Configure"\n' 'Options:\n' '1. Activate style (Make extraction-optimized active now)\n' '2. Reinstall (Change scope: user ↔ project)\n' '3. Uninstall (Remove all customizations)\n' '4. Keep as-is (No changes)\n\n' 'Based on selection, execute appropriate action' ) } return instructions def main(): """Main entry point for configuration script.""" # Check for command-line arguments if len(sys.argv) > 1: arg = sys.argv[1] if arg == '--install-user': success, path = install_output_style(scope='user') if success: print(f"\n✅ Output style installed (user-level)") print(f" Location: {path}") else: print(f"\n❌ Installation failed") sys.exit(0 if success else 1) elif arg == '--install-project': success, path = install_output_style(scope='project') if success: print(f"\n✅ Output style installed (project-level)") print(f" Location: {path}") else: print(f"\n❌ Installation failed") sys.exit(0 if success else 1) elif arg == '--install-statusline': success = install_status_line() if success: print(f"\n✅ Status line integration added") print(f" Location: ~/.claude/statusline.sh") else: print(f"\n❌ Status line installation failed") sys.exit(0 if success else 1) elif arg == '--uninstall': manifest = read_manifest() print("\n🗑️ Uninstalling Contextune customizations...\n") # Remove output style success, removed = uninstall_output_style(manifest) if removed: print(f"✅ Removed output style") # Remove status line success, removed = uninstall_status_line(manifest) if removed: print(f"✅ Removed status line integration") # Clear manifest clear_manifest() print(f"\n✅ Uninstallation complete!") print(f"\n⚠️ IMPORTANT: You can now safely disable the plugin:") print(f" /plugin disable contextune") print(f"\nTo reinstall later:") print(f" /plugin enable contextune") print(f" /ctx:configure") sys.exit(0) elif arg == '--uninstall-with-cleanup': manifest = read_manifest() print("\n🗑️ Uninstalling with cleanup...\n") # Remove output style uninstall_output_style(manifest) # Remove status line uninstall_status_line(manifest) # Clean .plans/ print(f"\n🗑️ Cleaning .plans/ directories...") removed_count = cleanup_plans_directories() print(f"✅ Removed {removed_count} .plans/ directories") # Clear manifest clear_manifest() print(f"\n✅ Complete uninstallation finished!") print(f"\n⚠️ You can now safely disable the plugin:") print(f" /plugin disable contextune") sys.exit(0) # No arguments - output instructions for Claude instructions = output_instructions_for_claude() print(json.dumps(instructions, indent=2)) sys.exit(0) if __name__ == '__main__': main()