Files
gh-shakes-tzd-contextune/commands/ctx-configure.py
2025-11-30 08:56:10 +08:00

484 lines
15 KiB
Python
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()