chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:22 +08:00
commit 9963ea7ef7
410 changed files with 85021 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
# SuperClaude PyPI Publishing Scripts
This directory contains scripts for building and publishing SuperClaude to PyPI.
## Scripts
### `publish.sh` - Main Publishing Script
Easy-to-use shell script for common publishing tasks:
```bash
# Test upload to TestPyPI
./scripts/publish.sh test
# Test installation from TestPyPI
./scripts/publish.sh test-install
# Production upload to PyPI
./scripts/publish.sh prod
# Build package only
./scripts/publish.sh build
# Clean build artifacts
./scripts/publish.sh clean
# Validate project structure
./scripts/publish.sh check
```
### `build_and_upload.py` - Advanced Build Script
Python script with detailed control over the build and upload process:
```bash
# Build and upload to TestPyPI
python scripts/build_and_upload.py --testpypi
# Test installation from TestPyPI
python scripts/build_and_upload.py --testpypi --test-install
# Production upload (with confirmation)
python scripts/build_and_upload.py
# Skip validation (for faster builds)
python scripts/build_and_upload.py --skip-validation --testpypi
# Clean only
python scripts/build_and_upload.py --clean
```
## Prerequisites
1. **PyPI Account**: Register at https://pypi.org/account/register/
2. **API Tokens**: Generate tokens at https://pypi.org/manage/account/
3. **Configuration**: Create `~/.pypirc`:
```ini
[pypi]
username = __token__
password = pypi-[your-production-token]
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-[your-test-token]
```
## GitHub Actions
The `.github/workflows/publish-pypi.yml` workflow automates publishing:
- **Automatic**: Publishes to PyPI when a GitHub release is created
- **Manual**: Can be triggered manually for TestPyPI uploads
- **Validation**: Includes package validation and installation testing
### Required Secrets
Set these in your GitHub repository settings → Secrets and variables → Actions:
- `PYPI_API_TOKEN`: Production PyPI token
- `TEST_PYPI_API_TOKEN`: TestPyPI token
## Publishing Workflow
### 1. Development Release (TestPyPI)
```bash
# Test the build and upload process
./scripts/publish.sh test
# Verify the package installs correctly
./scripts/publish.sh test-install
```
### 2. Production Release (PyPI)
#### Option A: Manual
```bash
# Upload directly (requires confirmation)
./scripts/publish.sh prod
```
#### Option B: GitHub Release (Recommended)
1. Update version in code
2. Commit and push changes
3. Create a new release on GitHub
4. GitHub Actions will automatically build and publish
## Version Management
Before publishing, ensure version consistency across:
- `pyproject.toml`
- `superclaude/__init__.py`
- `superclaude/__main__.py`
- `setup/__init__.py`
The build script validates version consistency automatically.
## Troubleshooting
### Build Failures
- Check Python version compatibility (≥3.8)
- Ensure all required files are present
- Validate `pyproject.toml` syntax
### Upload Failures
- Verify API tokens are correct
- Check if version already exists on PyPI
- Ensure package name is available
### Import Failures
- Check package structure (`__init__.py` files)
- Verify all dependencies are listed
- Test local installation first
## Security Notes
- Never commit API tokens to version control
- Use environment variables or `.pypirc` for credentials
- Tokens should have minimal required permissions
- Consider using Trusted Publishing for GitHub Actions
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env python3
"""
A/B Testing Framework for Workflow Variants
Compares two workflow variants with statistical significance testing.
Usage:
python scripts/ab_test_workflows.py \\
--variant-a progressive_v3_layer2 \\
--variant-b experimental_eager_layer3 \\
--metric tokens_used
"""
import argparse
import json
import statistics
from pathlib import Path
from typing import Dict, List, Tuple
from scipy import stats
class ABTestAnalyzer:
"""A/B testing framework for workflow optimization"""
def __init__(self, metrics_file: Path):
self.metrics_file = metrics_file
self.metrics: List[Dict] = []
self._load_metrics()
def _load_metrics(self):
"""Load metrics from JSONL file"""
if not self.metrics_file.exists():
print(f"Error: {self.metrics_file} not found")
return
with open(self.metrics_file, 'r') as f:
for line in f:
if line.strip():
self.metrics.append(json.loads(line))
def get_variant_metrics(self, workflow_id: str) -> List[Dict]:
"""Get all metrics for a specific workflow variant"""
return [m for m in self.metrics if m['workflow_id'] == workflow_id]
def extract_metric_values(self, metrics: List[Dict], metric: str) -> List[float]:
"""Extract specific metric values from metrics list"""
values = []
for m in metrics:
if metric in m:
value = m[metric]
# Handle boolean metrics
if isinstance(value, bool):
value = 1.0 if value else 0.0
values.append(float(value))
return values
def calculate_statistics(self, values: List[float]) -> Dict:
"""Calculate statistical measures"""
if not values:
return {
'count': 0,
'mean': 0,
'median': 0,
'stdev': 0,
'min': 0,
'max': 0
}
return {
'count': len(values),
'mean': statistics.mean(values),
'median': statistics.median(values),
'stdev': statistics.stdev(values) if len(values) > 1 else 0,
'min': min(values),
'max': max(values)
}
def perform_ttest(
self,
variant_a_values: List[float],
variant_b_values: List[float]
) -> Tuple[float, float]:
"""
Perform independent t-test between two variants.
Returns:
(t_statistic, p_value)
"""
if len(variant_a_values) < 2 or len(variant_b_values) < 2:
return 0.0, 1.0 # Not enough data
t_stat, p_value = stats.ttest_ind(variant_a_values, variant_b_values)
return t_stat, p_value
def determine_winner(
self,
variant_a_stats: Dict,
variant_b_stats: Dict,
p_value: float,
metric: str,
lower_is_better: bool = True
) -> str:
"""
Determine winning variant based on statistics.
Args:
variant_a_stats: Statistics for variant A
variant_b_stats: Statistics for variant B
p_value: Statistical significance (p-value)
metric: Metric being compared
lower_is_better: True if lower values are better (e.g., tokens_used)
Returns:
Winner description
"""
# Require statistical significance (p < 0.05)
if p_value >= 0.05:
return "No significant difference (p ≥ 0.05)"
# Require minimum sample size (20 trials per variant)
if variant_a_stats['count'] < 20 or variant_b_stats['count'] < 20:
return f"Insufficient data (need 20 trials, have {variant_a_stats['count']}/{variant_b_stats['count']})"
# Compare means
a_mean = variant_a_stats['mean']
b_mean = variant_b_stats['mean']
if lower_is_better:
if a_mean < b_mean:
improvement = ((b_mean - a_mean) / b_mean) * 100
return f"Variant A wins ({improvement:.1f}% better)"
else:
improvement = ((a_mean - b_mean) / a_mean) * 100
return f"Variant B wins ({improvement:.1f}% better)"
else:
if a_mean > b_mean:
improvement = ((a_mean - b_mean) / b_mean) * 100
return f"Variant A wins ({improvement:.1f}% better)"
else:
improvement = ((b_mean - a_mean) / a_mean) * 100
return f"Variant B wins ({improvement:.1f}% better)"
def generate_recommendation(
self,
winner: str,
variant_a_stats: Dict,
variant_b_stats: Dict,
p_value: float
) -> str:
"""Generate actionable recommendation"""
if "No significant difference" in winner:
return "⚖️ Keep current workflow (no improvement detected)"
if "Insufficient data" in winner:
return "📊 Continue testing (need more trials)"
if "Variant A wins" in winner:
return "✅ Keep Variant A as standard (statistically better)"
if "Variant B wins" in winner:
if variant_b_stats['mean'] > variant_a_stats['mean'] * 0.8: # At least 20% better
return "🚀 Promote Variant B to standard (significant improvement)"
else:
return "⚠️ Marginal improvement - continue testing before promotion"
return "🤔 Manual review recommended"
def compare_variants(
self,
variant_a_id: str,
variant_b_id: str,
metric: str = 'tokens_used',
lower_is_better: bool = True
) -> str:
"""
Compare two workflow variants on a specific metric.
Args:
variant_a_id: Workflow ID for variant A
variant_b_id: Workflow ID for variant B
metric: Metric to compare (default: tokens_used)
lower_is_better: True if lower values are better
Returns:
Comparison report
"""
# Get metrics for each variant
variant_a_metrics = self.get_variant_metrics(variant_a_id)
variant_b_metrics = self.get_variant_metrics(variant_b_id)
if not variant_a_metrics:
return f"Error: No data for variant A ({variant_a_id})"
if not variant_b_metrics:
return f"Error: No data for variant B ({variant_b_id})"
# Extract metric values
a_values = self.extract_metric_values(variant_a_metrics, metric)
b_values = self.extract_metric_values(variant_b_metrics, metric)
# Calculate statistics
a_stats = self.calculate_statistics(a_values)
b_stats = self.calculate_statistics(b_values)
# Perform t-test
t_stat, p_value = self.perform_ttest(a_values, b_values)
# Determine winner
winner = self.determine_winner(a_stats, b_stats, p_value, metric, lower_is_better)
# Generate recommendation
recommendation = self.generate_recommendation(winner, a_stats, b_stats, p_value)
# Format report
report = []
report.append("=" * 80)
report.append("A/B TEST COMPARISON REPORT")
report.append("=" * 80)
report.append("")
report.append(f"Metric: {metric}")
report.append(f"Better: {'Lower' if lower_is_better else 'Higher'} values")
report.append("")
report.append(f"## Variant A: {variant_a_id}")
report.append(f" Trials: {a_stats['count']}")
report.append(f" Mean: {a_stats['mean']:.2f}")
report.append(f" Median: {a_stats['median']:.2f}")
report.append(f" Std Dev: {a_stats['stdev']:.2f}")
report.append(f" Range: {a_stats['min']:.2f} - {a_stats['max']:.2f}")
report.append("")
report.append(f"## Variant B: {variant_b_id}")
report.append(f" Trials: {b_stats['count']}")
report.append(f" Mean: {b_stats['mean']:.2f}")
report.append(f" Median: {b_stats['median']:.2f}")
report.append(f" Std Dev: {b_stats['stdev']:.2f}")
report.append(f" Range: {b_stats['min']:.2f} - {b_stats['max']:.2f}")
report.append("")
report.append("## Statistical Significance")
report.append(f" t-statistic: {t_stat:.4f}")
report.append(f" p-value: {p_value:.4f}")
if p_value < 0.01:
report.append(" Significance: *** (p < 0.01) - Highly significant")
elif p_value < 0.05:
report.append(" Significance: ** (p < 0.05) - Significant")
elif p_value < 0.10:
report.append(" Significance: * (p < 0.10) - Marginally significant")
else:
report.append(" Significance: n.s. (p ≥ 0.10) - Not significant")
report.append("")
report.append(f"## Result: {winner}")
report.append(f"## Recommendation: {recommendation}")
report.append("")
report.append("=" * 80)
return "\n".join(report)
def main():
parser = argparse.ArgumentParser(description="A/B test workflow variants")
parser.add_argument(
'--variant-a',
required=True,
help='Workflow ID for variant A'
)
parser.add_argument(
'--variant-b',
required=True,
help='Workflow ID for variant B'
)
parser.add_argument(
'--metric',
default='tokens_used',
help='Metric to compare (default: tokens_used)'
)
parser.add_argument(
'--higher-is-better',
action='store_true',
help='Higher values are better (default: lower is better)'
)
parser.add_argument(
'--output',
help='Output file (default: stdout)'
)
args = parser.parse_args()
# Find metrics file
metrics_file = Path('docs/memory/workflow_metrics.jsonl')
analyzer = ABTestAnalyzer(metrics_file)
report = analyzer.compare_variants(
args.variant_a,
args.variant_b,
args.metric,
lower_is_better=not args.higher_is_better
)
if args.output:
with open(args.output, 'w') as f:
f.write(report)
print(f"Report written to {args.output}")
else:
print(report)
if __name__ == '__main__':
main()
+331
View File
@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
Workflow Metrics Analysis Script
Analyzes workflow_metrics.jsonl for continuous optimization and A/B testing.
Usage:
python scripts/analyze_workflow_metrics.py --period week
python scripts/analyze_workflow_metrics.py --period month
python scripts/analyze_workflow_metrics.py --task-type bug_fix
"""
import argparse
import json
import statistics
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List
class WorkflowMetricsAnalyzer:
"""Analyze workflow metrics for optimization"""
def __init__(self, metrics_file: Path):
self.metrics_file = metrics_file
self.metrics: List[Dict] = []
self._load_metrics()
def _load_metrics(self):
"""Load metrics from JSONL file"""
if not self.metrics_file.exists():
print(f"Warning: {self.metrics_file} not found")
return
with open(self.metrics_file, 'r') as f:
for line in f:
if line.strip():
self.metrics.append(json.loads(line))
print(f"Loaded {len(self.metrics)} metric records")
def filter_by_period(self, period: str) -> List[Dict]:
"""Filter metrics by time period"""
now = datetime.now()
if period == "week":
cutoff = now - timedelta(days=7)
elif period == "month":
cutoff = now - timedelta(days=30)
elif period == "all":
return self.metrics
else:
raise ValueError(f"Invalid period: {period}")
filtered = [
m for m in self.metrics
if datetime.fromisoformat(m['timestamp']) >= cutoff
]
print(f"Filtered to {len(filtered)} records in last {period}")
return filtered
def analyze_by_task_type(self, metrics: List[Dict]) -> Dict:
"""Analyze metrics grouped by task type"""
by_task = defaultdict(list)
for m in metrics:
by_task[m['task_type']].append(m)
results = {}
for task_type, task_metrics in by_task.items():
results[task_type] = {
'count': len(task_metrics),
'avg_tokens': statistics.mean(m['tokens_used'] for m in task_metrics),
'avg_time_ms': statistics.mean(m['time_ms'] for m in task_metrics),
'success_rate': sum(m['success'] for m in task_metrics) / len(task_metrics) * 100,
'avg_files_read': statistics.mean(m.get('files_read', 0) for m in task_metrics),
}
return results
def analyze_by_complexity(self, metrics: List[Dict]) -> Dict:
"""Analyze metrics grouped by complexity level"""
by_complexity = defaultdict(list)
for m in metrics:
by_complexity[m['complexity']].append(m)
results = {}
for complexity, comp_metrics in by_complexity.items():
results[complexity] = {
'count': len(comp_metrics),
'avg_tokens': statistics.mean(m['tokens_used'] for m in comp_metrics),
'avg_time_ms': statistics.mean(m['time_ms'] for m in comp_metrics),
'success_rate': sum(m['success'] for m in comp_metrics) / len(comp_metrics) * 100,
}
return results
def analyze_by_workflow(self, metrics: List[Dict]) -> Dict:
"""Analyze metrics grouped by workflow variant"""
by_workflow = defaultdict(list)
for m in metrics:
by_workflow[m['workflow_id']].append(m)
results = {}
for workflow_id, wf_metrics in by_workflow.items():
results[workflow_id] = {
'count': len(wf_metrics),
'avg_tokens': statistics.mean(m['tokens_used'] for m in wf_metrics),
'median_tokens': statistics.median(m['tokens_used'] for m in wf_metrics),
'avg_time_ms': statistics.mean(m['time_ms'] for m in wf_metrics),
'success_rate': sum(m['success'] for m in wf_metrics) / len(wf_metrics) * 100,
}
return results
def identify_best_workflows(self, metrics: List[Dict]) -> Dict[str, str]:
"""Identify best workflow for each task type"""
by_task_workflow = defaultdict(lambda: defaultdict(list))
for m in metrics:
by_task_workflow[m['task_type']][m['workflow_id']].append(m)
best_workflows = {}
for task_type, workflows in by_task_workflow.items():
best_workflow = None
best_score = float('inf')
for workflow_id, wf_metrics in workflows.items():
# Score = avg_tokens (lower is better)
avg_tokens = statistics.mean(m['tokens_used'] for m in wf_metrics)
success_rate = sum(m['success'] for m in wf_metrics) / len(wf_metrics)
# Only consider if success rate >= 95%
if success_rate >= 0.95:
if avg_tokens < best_score:
best_score = avg_tokens
best_workflow = workflow_id
if best_workflow:
best_workflows[task_type] = best_workflow
return best_workflows
def identify_inefficiencies(self, metrics: List[Dict]) -> List[Dict]:
"""Identify inefficient patterns"""
inefficiencies = []
# Expected token budgets by complexity
budgets = {
'ultra-light': 800,
'light': 2000,
'medium': 5000,
'heavy': 20000,
'ultra-heavy': 50000
}
for m in metrics:
issues = []
# Check token budget overrun
expected_budget = budgets.get(m['complexity'], 5000)
if m['tokens_used'] > expected_budget * 1.3: # 30% over budget
issues.append(f"Token overrun: {m['tokens_used']} vs {expected_budget}")
# Check success rate
if not m['success']:
issues.append("Task failed")
# Check time performance (light tasks should be fast)
if m['complexity'] in ['ultra-light', 'light'] and m['time_ms'] > 10000:
issues.append(f"Slow execution: {m['time_ms']}ms for {m['complexity']} task")
if issues:
inefficiencies.append({
'timestamp': m['timestamp'],
'task_type': m['task_type'],
'complexity': m['complexity'],
'workflow_id': m['workflow_id'],
'issues': issues
})
return inefficiencies
def calculate_token_savings(self, metrics: List[Dict]) -> Dict:
"""Calculate token savings vs unlimited baseline"""
# Unlimited baseline estimates
baseline = {
'ultra-light': 1000,
'light': 2500,
'medium': 7500,
'heavy': 30000,
'ultra-heavy': 100000
}
total_actual = 0
total_baseline = 0
for m in metrics:
total_actual += m['tokens_used']
total_baseline += baseline.get(m['complexity'], 7500)
savings = total_baseline - total_actual
savings_percent = (savings / total_baseline * 100) if total_baseline > 0 else 0
return {
'total_actual': total_actual,
'total_baseline': total_baseline,
'total_savings': savings,
'savings_percent': savings_percent
}
def generate_report(self, period: str) -> str:
"""Generate comprehensive analysis report"""
metrics = self.filter_by_period(period)
if not metrics:
return "No metrics available for analysis"
report = []
report.append("=" * 80)
report.append(f"WORKFLOW METRICS ANALYSIS REPORT - Last {period}")
report.append("=" * 80)
report.append("")
# Overall statistics
report.append("## Overall Statistics")
report.append(f"Total Tasks: {len(metrics)}")
report.append(f"Success Rate: {sum(m['success'] for m in metrics) / len(metrics) * 100:.1f}%")
report.append(f"Avg Tokens: {statistics.mean(m['tokens_used'] for m in metrics):.0f}")
report.append(f"Avg Time: {statistics.mean(m['time_ms'] for m in metrics):.0f}ms")
report.append("")
# Token savings
savings = self.calculate_token_savings(metrics)
report.append("## Token Efficiency")
report.append(f"Actual Usage: {savings['total_actual']:,} tokens")
report.append(f"Unlimited Baseline: {savings['total_baseline']:,} tokens")
report.append(f"Total Savings: {savings['total_savings']:,} tokens ({savings['savings_percent']:.1f}%)")
report.append("")
# By task type
report.append("## Analysis by Task Type")
by_task = self.analyze_by_task_type(metrics)
for task_type, stats in sorted(by_task.items()):
report.append(f"\n### {task_type}")
report.append(f" Count: {stats['count']}")
report.append(f" Avg Tokens: {stats['avg_tokens']:.0f}")
report.append(f" Avg Time: {stats['avg_time_ms']:.0f}ms")
report.append(f" Success Rate: {stats['success_rate']:.1f}%")
report.append(f" Avg Files Read: {stats['avg_files_read']:.1f}")
report.append("")
# By complexity
report.append("## Analysis by Complexity")
by_complexity = self.analyze_by_complexity(metrics)
for complexity in ['ultra-light', 'light', 'medium', 'heavy', 'ultra-heavy']:
if complexity in by_complexity:
stats = by_complexity[complexity]
report.append(f"\n### {complexity}")
report.append(f" Count: {stats['count']}")
report.append(f" Avg Tokens: {stats['avg_tokens']:.0f}")
report.append(f" Success Rate: {stats['success_rate']:.1f}%")
report.append("")
# Best workflows
report.append("## Best Workflows per Task Type")
best = self.identify_best_workflows(metrics)
for task_type, workflow_id in sorted(best.items()):
report.append(f" {task_type}: {workflow_id}")
report.append("")
# Inefficiencies
inefficiencies = self.identify_inefficiencies(metrics)
if inefficiencies:
report.append("## Inefficiencies Detected")
report.append(f"Total Issues: {len(inefficiencies)}")
for issue in inefficiencies[:5]: # Show top 5
report.append(f"\n {issue['timestamp']}")
report.append(f" Task: {issue['task_type']} ({issue['complexity']})")
report.append(f" Workflow: {issue['workflow_id']}")
for problem in issue['issues']:
report.append(f" - {problem}")
report.append("")
report.append("=" * 80)
return "\n".join(report)
def main():
parser = argparse.ArgumentParser(description="Analyze workflow metrics")
parser.add_argument(
'--period',
choices=['week', 'month', 'all'],
default='week',
help='Analysis time period'
)
parser.add_argument(
'--task-type',
help='Filter by specific task type'
)
parser.add_argument(
'--output',
help='Output file (default: stdout)'
)
args = parser.parse_args()
# Find metrics file
metrics_file = Path('docs/memory/workflow_metrics.jsonl')
analyzer = WorkflowMetricsAnalyzer(metrics_file)
report = analyzer.generate_report(args.period)
if args.output:
with open(args.output, 'w') as f:
f.write(report)
print(f"Report written to {args.output}")
else:
print(report)
if __name__ == '__main__':
main()
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""
Build SuperClaude plugin distribution artefacts from unified sources.
Usage:
python scripts/build_superclaude_plugin.py
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PLUGIN_SRC = ROOT / "plugins" / "superclaude"
DIST_ROOT = ROOT / "dist" / "plugins" / "superclaude"
MANIFEST_DIR = PLUGIN_SRC / "manifest"
def load_metadata() -> dict:
with (MANIFEST_DIR / "metadata.json").open() as f:
metadata = json.load(f)
version_file = ROOT / "VERSION"
if version_file.exists():
metadata["plugin_version"] = version_file.read_text().strip()
else:
# Fall back to metadata override or default version
metadata["plugin_version"] = metadata.get("plugin_version", "0.0.0")
metadata.setdefault("keywords", [])
return metadata
def render_template(template_path: Path, placeholders: dict[str, str]) -> str:
content = template_path.read_text()
for key, value in placeholders.items():
token = f"{{{{{key}}}}}"
content = content.replace(token, value)
return content
def copy_tree(src: Path, dest: Path) -> None:
if not src.exists():
return
shutil.copytree(src, dest, dirs_exist_ok=True)
def main() -> None:
if not PLUGIN_SRC.exists():
raise SystemExit(f"Missing plugin sources: {PLUGIN_SRC}")
metadata = load_metadata()
placeholders = {
"plugin_name": metadata["plugin_name"],
"plugin_version": metadata["plugin_version"],
"plugin_description": metadata["plugin_description"],
"author_name": metadata["author_name"],
"homepage_url": metadata["homepage_url"],
"repository_url": metadata["repository_url"],
"license": metadata["license"],
"keywords_json": json.dumps(metadata["keywords"]),
"marketplace_name": metadata["marketplace_name"],
"marketplace_description": metadata["marketplace_description"],
}
# Clean dist directory
if DIST_ROOT.exists():
shutil.rmtree(DIST_ROOT)
DIST_ROOT.mkdir(parents=True, exist_ok=True)
# Copy top-level asset directories
for folder in ["agents", "commands", "hooks", "scripts", "skills"]:
copy_tree(PLUGIN_SRC / folder, DIST_ROOT / folder)
# Render manifests
claude_dir = DIST_ROOT / ".claude-plugin"
claude_dir.mkdir(parents=True, exist_ok=True)
plugin_manifest = render_template(MANIFEST_DIR / "plugin.template.json", placeholders)
(claude_dir / "plugin.json").write_text(plugin_manifest + "\n")
marketplace_manifest = render_template(MANIFEST_DIR / "marketplace.template.json", placeholders)
(claude_dir / "marketplace.json").write_text(marketplace_manifest + "\n")
# Copy tests into manifest directory
tests_src = PLUGIN_SRC / "tests"
if tests_src.exists():
copy_tree(tests_src, claude_dir / "tests")
print(f"✅ Built plugin artefacts at {DIST_ROOT}")
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
# SuperClaude Project Cleanup Script
# Removes build artifacts, cache files, and temporary files
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
echo -e "${BLUE}🧹 SuperClaude Project Cleanup${NC}"
echo -e "📁 Project root: $PROJECT_ROOT"
cd "$PROJECT_ROOT"
# Function to safely remove files/directories
safe_remove() {
local target="$1"
local description="$2"
if [ -e "$target" ]; then
rm -rf "$target"
echo -e "${GREEN}✅ Removed $description${NC}"
else
echo -e "${YELLOW}$description not found (already clean)${NC}"
fi
}
echo -e "\n${YELLOW}🗑️ Removing Python cache files...${NC}"
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
find . -name "*.pyc" -type f -delete 2>/dev/null || true
find . -name "*.pyo" -type f -delete 2>/dev/null || true
find . -name "*.pyd" -type f -delete 2>/dev/null || true
echo -e "${GREEN}✅ Python cache files cleaned${NC}"
echo -e "\n${YELLOW}📦 Removing build artifacts...${NC}"
safe_remove "build/" "Build directory"
safe_remove "dist/" "Distribution directory"
safe_remove "*.egg-info" "Egg-info directories"
safe_remove ".eggs/" "Eggs directory"
safe_remove "wheels/" "Wheels directory"
safe_remove "pip-wheel-metadata/" "Pip wheel metadata"
echo -e "\n${YELLOW}🧪 Removing test artifacts...${NC}"
safe_remove ".pytest_cache/" "Pytest cache"
safe_remove ".tox/" "Tox directory"
safe_remove ".nox/" "Nox directory"
safe_remove "htmlcov/" "HTML coverage reports"
safe_remove ".coverage" "Coverage data file"
safe_remove "coverage.xml" "Coverage XML report"
safe_remove ".hypothesis/" "Hypothesis directory"
echo -e "\n${YELLOW}🔧 Removing development tool cache...${NC}"
safe_remove ".mypy_cache/" "MyPy cache"
safe_remove ".ruff_cache/" "Ruff cache"
safe_remove ".black/" "Black cache"
echo -e "\n${YELLOW}🗄️ Removing temporary files...${NC}"
find . -name "*.tmp" -type f -delete 2>/dev/null || true
find . -name "*.temp" -type f -delete 2>/dev/null || true
find . -name "*~" -type f -delete 2>/dev/null || true
find . -name "*.bak" -type f -delete 2>/dev/null || true
find . -name "*.backup" -type f -delete 2>/dev/null || true
echo -e "${GREEN}✅ Temporary files cleaned${NC}"
echo -e "\n${YELLOW}📋 Removing PyPI publishing artifacts...${NC}"
safe_remove "twine.log" "Twine log file"
safe_remove ".twine/" "Twine directory"
safe_remove "PYPI_SETUP_COMPLETE.md" "Setup completion file"
echo -e "\n${YELLOW}🧽 Removing OS-specific files...${NC}"
find . -name ".DS_Store" -type f -delete 2>/dev/null || true
find . -name "._*" -type f -delete 2>/dev/null || true
find . -name "Thumbs.db" -type f -delete 2>/dev/null || true
find . -name "Desktop.ini" -type f -delete 2>/dev/null || true
echo -e "${GREEN}✅ OS-specific files cleaned${NC}"
echo -e "\n${GREEN}🎉 Cleanup completed successfully!${NC}"
echo -e "${BLUE}📊 Project is clean and ready for development or publishing${NC}"
# Show summary
echo -e "\n${BLUE}📈 Summary:${NC}"
echo -e " • Python cache files: Removed"
echo -e " • Build artifacts: Cleaned"
echo -e " • Test artifacts: Removed"
echo -e " • Development tool cache: Cleared"
echo -e " • Temporary files: Deleted"
echo -e " • PyPI artifacts: Cleaned"
echo -e " • OS-specific files: Removed"
echo -e "\n${YELLOW}💡 Next steps:${NC}"
echo -e " • Run validation: ${BLUE}python3 scripts/validate_pypi_ready.py${NC}"
echo -e " • Test build: ${BLUE}./scripts/publish.sh build${NC}"
echo -e " • Check git status: ${BLUE}git status${NC}"
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
"""
SuperClaude PyPI Publishing Helper Script
Easy-to-use wrapper for the Python build and upload script
"""
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
BUILD_SCRIPT="$SCRIPT_DIR/build_and_upload.py"
echo -e "${BLUE}🚀 SuperClaude PyPI Publishing Helper${NC}"
echo -e "📁 Project root: $PROJECT_ROOT"
# Function to show usage
show_usage() {
echo -e "${YELLOW}Usage:${NC}"
echo " $0 test - Build and upload to TestPyPI"
echo " $0 test-install - Test installation from TestPyPI"
echo " $0 prod - Build and upload to production PyPI"
echo " $0 build - Only build the package"
echo " $0 clean - Clean build artifacts"
echo " $0 check - Validate project structure only"
echo ""
echo -e "${YELLOW}Examples:${NC}"
echo " $0 test # Upload to TestPyPI"
echo " $0 test && $0 test-install # Upload and test"
echo " $0 prod # Upload to production"
}
# Check if Python script exists
if [ ! -f "$BUILD_SCRIPT" ]; then
echo -e "${RED}❌ Build script not found: $BUILD_SCRIPT${NC}"
exit 1
fi
# Parse command
case "${1:-}" in
"test")
echo -e "${YELLOW}📦 Building and uploading to TestPyPI...${NC}"
python3 "$BUILD_SCRIPT" --testpypi
echo -e "${GREEN}✅ Uploaded to TestPyPI! Test with:${NC}"
echo -e " pip install --index-url https://test.pypi.org/simple/ SuperClaude"
;;
"test-install")
echo -e "${YELLOW}🧪 Testing installation from TestPyPI...${NC}"
python3 "$BUILD_SCRIPT" --testpypi --test-install --skip-build
;;
"prod"|"production")
echo -e "${YELLOW}🚨 Building and uploading to PRODUCTION PyPI...${NC}"
echo -e "${RED}⚠️ This cannot be undone!${NC}"
python3 "$BUILD_SCRIPT"
echo -e "${GREEN}✅ Uploaded to PyPI! Install with:${NC}"
echo -e " pip install SuperClaude"
;;
"build")
echo -e "${YELLOW}🔨 Building package only...${NC}"
python3 "$BUILD_SCRIPT" --skip-validation --testpypi --skip-upload
echo -e "${GREEN}✅ Package built in dist/ directory${NC}"
;;
"clean")
echo -e "${YELLOW}🧹 Cleaning build artifacts...${NC}"
python3 "$BUILD_SCRIPT" --clean
echo -e "${GREEN}✅ Build artifacts cleaned${NC}"
;;
"check"|"validate")
echo -e "${YELLOW}🔍 Validating project structure...${NC}"
python3 "$BUILD_SCRIPT" --skip-build --testpypi
;;
"help"|"-h"|"--help"|"")
show_usage
;;
*)
echo -e "${RED}❌ Unknown command: $1${NC}"
echo ""
show_usage
exit 1
;;
esac
+959
View File
@@ -0,0 +1,959 @@
#!/usr/bin/env python3
"""
SuperClaude Framework Sync Script
Automated pull-sync with namespace isolation for Plugin distribution
This script synchronizes content from SuperClaude_Framework repository and
transforms it for distribution as a Claude Code plugin with proper namespace
isolation (sc: prefix for commands, sc- prefix for filenames).
Usage:
python scripts/sync_from_framework.py [OPTIONS]
Options:
--framework-repo URL Framework repository URL
--plugin-root PATH Plugin repository root path
--dry-run Preview changes without applying
--output-report PATH Save sync report to file
"""
import sys
import argparse
import tempfile
import shutil
import hashlib
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import json
import re
import subprocess
from dataclasses import dataclass, asdict
from datetime import datetime
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class ProtectionViolationError(RuntimeError):
"""Raised when sync would overwrite a Plugin-owned file listed in PROTECTED_PATHS."""
pass
@dataclass
class SyncResult:
"""Results from sync operation."""
success: bool
timestamp: str
framework_commit: str
framework_version: str
files_synced: int
files_modified: int
commands_transformed: int
agents_transformed: int
mcp_servers_merged: int
warnings: List[str]
errors: List[str]
def to_dict(self) -> dict:
return asdict(self)
class ContentTransformer:
"""Transforms Framework content for Plugin namespace."""
# Regex patterns for transformation
COMMAND_HEADER_PATTERN = re.compile(r'^(#+\s+)/(\w+)', re.MULTILINE)
COMMAND_REF_PATTERN = re.compile(r'(?<![/\w])/(\w+)(?=\s|$|:|`|\)|\])')
LINK_REF_PATTERN = re.compile(r'\[/(\w+)\]')
FRONTMATTER_NAME_PATTERN = re.compile(r'^name:\s*(.+)$', re.MULTILINE)
@staticmethod
def transform_command(content: str, filename: str) -> str:
"""
Transform command content for sc: namespace.
Transformations:
- Header: # /brainstorm → # /sc:brainstorm
- References: /analyze → /sc:analyze
- Links: [/task] → [/sc:task]
Args:
content: Original command file content
filename: Command filename (for logging)
Returns:
Transformed content with sc: namespace
"""
logger.debug(f"Transforming command: {filename}")
# Transform main header
content = ContentTransformer.COMMAND_HEADER_PATTERN.sub(
r'\1/sc:\2',
content
)
# Transform command references in text
content = ContentTransformer.COMMAND_REF_PATTERN.sub(
r'/sc:\1',
content
)
# Transform command references in links
content = ContentTransformer.LINK_REF_PATTERN.sub(
r'[/sc:\1]',
content
)
return content
@staticmethod
def transform_agent(content: str, filename: str) -> str:
"""
Transform agent frontmatter name.
Transformations:
- name: backend-architect → name: sc-backend-architect
Args:
content: Original agent file content
filename: Agent filename (for logging)
Returns:
Transformed content with sc- prefix in name field
"""
logger.debug(f"Transforming agent: {filename}")
# Parse frontmatter
frontmatter_pattern = re.compile(
r'^---\n(.*?)\n---',
re.DOTALL | re.MULTILINE
)
match = frontmatter_pattern.search(content)
if not match:
logger.warning(f"No frontmatter found in agent: {filename}")
return content
frontmatter = match.group(1)
# Transform name field (add sc- prefix if not already present)
def add_prefix(match):
name = match.group(1).strip()
if not name.startswith('sc-'):
return f'name: sc-{name}'
return match.group(0)
frontmatter = ContentTransformer.FRONTMATTER_NAME_PATTERN.sub(
add_prefix,
frontmatter
)
# Replace frontmatter
content = frontmatter_pattern.sub(
f'---\n{frontmatter}\n---',
content,
count=1
)
return content
class FileSyncer:
"""Handles file synchronization with git integration."""
def __init__(self, plugin_root: Path, dry_run: bool = False):
self.plugin_root = plugin_root
self.dry_run = dry_run
self.git_available = self._check_git()
def _check_git(self) -> bool:
"""Check if git is available and repo is initialized."""
try:
subprocess.run(
['git', 'rev-parse', '--git-dir'],
cwd=self.plugin_root,
capture_output=True,
check=True
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
logger.warning("Git not available - file operations will not preserve history")
return False
def sync_directory(
self,
source_dir: Path,
dest_dir: Path,
filename_prefix: str = "",
transform_fn=None
) -> Dict[str, int]:
"""
Sync directory with namespace prefix and transformation.
Args:
source_dir: Source directory path
dest_dir: Destination directory path
filename_prefix: Prefix to add to filenames (e.g., 'sc-')
transform_fn: Optional content transformation function
Returns:
Statistics dict with counts of synced/modified files
"""
stats = {'synced': 0, 'modified': 0, 'renamed': 0}
if not source_dir.exists():
logger.warning(f"Source directory not found: {source_dir}")
return stats
dest_dir.mkdir(parents=True, exist_ok=True)
# Get existing files in dest (with sc- prefix)
existing_files = {f.name: f for f in dest_dir.glob('*.md')}
synced_files = set()
for source_file in source_dir.glob('*.md'):
# Apply filename prefix
new_name = f"{filename_prefix}{source_file.name}"
synced_files.add(new_name)
dest_file = dest_dir / new_name
# Read and transform content
content = source_file.read_text(encoding='utf-8')
if transform_fn:
content = transform_fn(content, source_file.name)
# Check if file exists with different name (needs git mv)
old_unprefixed = source_file.name
old_file_path = dest_dir / old_unprefixed
if old_file_path.exists() and new_name != old_unprefixed:
# File needs renaming: use git mv to preserve history
if self.git_available:
self._git_mv(old_file_path, dest_file)
stats['renamed'] += 1
else:
# Fallback to regular rename
if not self.dry_run:
old_file_path.rename(dest_file)
stats['renamed'] += 1
logger.info(f" 📝 Renamed: {old_unprefixed}{new_name}")
# Write content
if not self.dry_run:
dest_file.write_text(content, encoding='utf-8')
if dest_file.exists():
stats['modified'] += 1
else:
stats['synced'] += 1
# Remove files that no longer exist in source
# (only remove files with prefix that aren't in synced set)
for filename, filepath in existing_files.items():
if filename.startswith(filename_prefix) and filename not in synced_files:
if not self.dry_run:
filepath.unlink()
logger.info(f" 🗑️ Removed: {filepath.relative_to(self.plugin_root)}")
return stats
def _git_mv(self, old_path: Path, new_path: Path):
"""Use git mv to preserve history."""
if self.dry_run:
logger.info(f" [DRY RUN] git mv {old_path.name} {new_path.name}")
return
try:
subprocess.run(
['git', 'mv', str(old_path), str(new_path)],
cwd=self.plugin_root,
check=True,
capture_output=True
)
logger.info(f" 📝 Renamed (git mv): {old_path.name}{new_path.name}")
except subprocess.CalledProcessError as e:
# Fallback to regular rename
logger.warning(f" ⚠️ Git mv failed, using regular rename: {e}")
old_path.rename(new_path)
def copy_directory(self, source_dir: Path, dest_dir: Path) -> int:
"""
Copy directory contents as-is (no transformation).
Args:
source_dir: Source directory path
dest_dir: Destination directory path
Returns:
Number of files copied
"""
if not source_dir.exists():
logger.warning(f"Source directory not found: {source_dir}")
return 0
dest_dir.mkdir(parents=True, exist_ok=True)
count = 0
for source_file in source_dir.glob('**/*'):
if source_file.is_file():
rel_path = source_file.relative_to(source_dir)
dest_file = dest_dir / rel_path
dest_file.parent.mkdir(parents=True, exist_ok=True)
if not self.dry_run:
shutil.copy2(source_file, dest_file)
count += 1
logger.debug(f" 📄 Copied: {rel_path}")
return count
class PluginJsonGenerator:
"""Generates .claude-plugin/plugin.json from synced commands."""
def __init__(self, plugin_root: Path):
self.plugin_root = plugin_root
def generate(self, framework_version: str) -> dict:
"""
Generate plugin.json with command mappings.
Args:
framework_version: Version from Framework repository
Returns:
Complete plugin.json dictionary
"""
commands_dir = self.plugin_root / 'commands'
# Base metadata from existing plugin.json
root_plugin_json = self.plugin_root / 'plugin.json'
if root_plugin_json.exists():
base_metadata = json.loads(root_plugin_json.read_text())
else:
base_metadata = {
"name": "sc",
"description": "SuperClaude Plugin",
"author": {"name": "SuperClaude Team"},
"license": "MIT"
}
# Build command mappings
commands = {}
if commands_dir.exists():
for cmd_file in sorted(commands_dir.glob('sc-*.md')):
# Extract command name from filename
# sc-brainstorm.md → brainstorm
cmd_name = cmd_file.stem.replace('sc-', '')
# Map sc:brainstorm to path
commands[f"sc:{cmd_name}"] = f"commands/{cmd_file.name}"
plugin_json = {
"name": "sc",
"version": framework_version,
"description": base_metadata.get("description", ""),
"author": base_metadata.get("author", {}),
"homepage": base_metadata.get("homepage", ""),
"repository": base_metadata.get("repository", ""),
"license": base_metadata.get("license", "MIT"),
"keywords": base_metadata.get("keywords", [])
}
logger.info(f"✅ Generated plugin.json with {len(commands)} commands")
return plugin_json
def write(self, plugin_json: dict, dry_run: bool = False):
"""Write plugin.json to .claude-plugin/ directory."""
output_path = self.plugin_root / '.claude-plugin' / 'plugin.json'
output_path.parent.mkdir(parents=True, exist_ok=True)
if dry_run:
logger.info(f"[DRY RUN] Would write plugin.json to: {output_path}")
logger.info(json.dumps(plugin_json, indent=2))
return
output_path.write_text(
json.dumps(plugin_json, indent=2) + '\n',
encoding='utf-8'
)
logger.info(f"✅ Written: {output_path}")
class McpMerger:
"""Safely merges MCP server configurations."""
def __init__(self, plugin_root: Path):
self.plugin_root = plugin_root
def merge(
self,
framework_mcp: dict,
plugin_mcp: dict
) -> Tuple[dict, List[str]]:
"""
Merge MCP configurations with conflict detection.
Strategy:
- Framework servers take precedence
- Preserve Plugin-specific servers
- Log warnings for conflicts
Args:
framework_mcp: MCP servers from Framework
plugin_mcp: MCP servers from Plugin
Returns:
(merged_config, warnings)
"""
merged = {}
warnings = []
# Add Framework servers (source of truth)
for name, config in framework_mcp.items():
merged[name] = config
# Add Plugin-specific servers if not in Framework
for name, config in plugin_mcp.items():
if name not in merged:
merged[name] = config
warnings.append(
f"Preserved plugin-specific MCP server: {name}"
)
else:
# Check if configurations differ
if config != merged[name]:
warnings.append(
f"MCP server '{name}' conflict - using Framework version"
)
return merged, warnings
def backup_current(self) -> Optional[Path]:
"""Create backup of current plugin.json."""
plugin_json = self.plugin_root / 'plugin.json'
if not plugin_json.exists():
return None
backup_dir = self.plugin_root / 'backups'
backup_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_path = backup_dir / f'plugin.json.{timestamp}.backup'
shutil.copy2(plugin_json, backup_path)
logger.info(f"📦 Backup created: {backup_path}")
return backup_path
class FrameworkSyncer:
"""Main orchestrator for Framework → Plugin sync."""
# ── SYNC MAPPINGS ──────────────────────────────────────────────────────────
# What to pull from Framework and transform for Plugin distribution.
# Symmetric pair with PROTECTED_PATHS below: a path appears in one or the other,
# never both.
SYNC_MAPPINGS = {
"src/superclaude/commands": "commands", # /cmd → /sc:cmd, sc- prefix
"src/superclaude/agents": "agents", # name → sc-name in frontmatter
# core/ and modes/ are intentionally absent — they live in PROTECTED_PATHS
}
# ── PROTECTED PATHS ────────────────────────────────────────────────────────
# Plugin-owned files and directories that must NEVER be overwritten by sync,
# regardless of what the Framework contains.
#
# Algorithm: before sync → hash all protected paths → after sync → re-hash
# and raise ProtectionViolationError if anything changed.
#
# To move a path from protected to synced: remove it here, add to SYNC_MAPPINGS.
PROTECTED_PATHS: List[str] = [
# Plugin-specific documentation (Plugin spec, not Framework spec)
"README.md",
"README-ja.md",
"README-zh.md",
"BACKUP_GUIDE.md",
"MIGRATION_GUIDE.md",
"SECURITY.md",
"CLAUDE.md",
"LICENSE",
".gitignore",
# Plugin configuration & marketplace metadata
".claude-plugin/",
# Plugin infrastructure (workflows, scripts, tests are Plugin-owned)
".github/",
"docs/",
"scripts/",
"tests/",
"backups/",
# Plugin-customized behavioral content
# Plugin maintains its own tuned versions; Framework versions are ignored.
"core/",
"modes/",
]
def __init__(
self,
framework_repo: str,
plugin_root: Path,
dry_run: bool = False
):
self.framework_repo = framework_repo
self.plugin_root = plugin_root
self.dry_run = dry_run
self.temp_dir = None
self.warnings = []
self.errors = []
def sync(self) -> SyncResult:
"""Execute full sync workflow."""
try:
logger.info("🔄 Starting Framework sync...")
# Step 1: Clone Framework
framework_path = self._clone_framework()
framework_commit = self._get_commit_hash(framework_path)
framework_version = self._get_version(framework_path)
logger.info(f"📦 Framework version: {framework_version}")
logger.info(f"📝 Framework commit: {framework_commit[:8]}")
# Step 2: Snapshot protected files BEFORE any changes
protection_snapshot = self._snapshot_protected_files()
# Step 3: Create backup
self._create_backup()
# Step 4: Transform and sync content
stats = self._sync_content(framework_path)
# Step 5: Verify protected files were NOT touched
self._validate_protected_files(protection_snapshot)
# Step 6: Generate plugin.json
self._generate_plugin_json(framework_version)
# Step 7: Merge MCP configurations
mcp_merged = self._merge_mcp_configs(framework_path)
# Step 8: Validate sync results
self._validate_sync()
logger.info("✅ Sync completed successfully!")
return SyncResult(
success=True,
timestamp=datetime.now().isoformat(),
framework_commit=framework_commit,
framework_version=framework_version,
files_synced=stats['files_synced'],
files_modified=stats['files_modified'],
commands_transformed=stats['commands'],
agents_transformed=stats['agents'],
mcp_servers_merged=mcp_merged,
warnings=self.warnings,
errors=self.errors
)
except ProtectionViolationError as e:
# Protection violations are logged already; surface them clearly in the report
self.errors.append(str(e))
return SyncResult(
success=False,
timestamp=datetime.now().isoformat(),
framework_commit="",
framework_version="",
files_synced=0,
files_modified=0,
commands_transformed=0,
agents_transformed=0,
mcp_servers_merged=0,
warnings=self.warnings,
errors=self.errors
)
except Exception as e:
logger.error(f"❌ Sync failed: {e}", exc_info=True)
self.errors.append(str(e))
return SyncResult(
success=False,
timestamp=datetime.now().isoformat(),
framework_commit="",
framework_version="",
files_synced=0,
files_modified=0,
commands_transformed=0,
agents_transformed=0,
mcp_servers_merged=0,
warnings=self.warnings,
errors=self.errors
)
finally:
self._cleanup()
# ── Protection helpers ─────────────────────────────────────────────────────
@staticmethod
def _hash_file(path: Path) -> str:
"""Return SHA-256 hex digest of a file's contents."""
h = hashlib.sha256()
h.update(path.read_bytes())
return h.hexdigest()
def _snapshot_protected_files(self) -> Dict[str, str]:
"""
Hash every file that lives under a PROTECTED_PATHS entry.
Called BEFORE sync begins so we have a baseline to compare against.
Returns:
Mapping of relative-path-string → SHA-256 hex digest.
"""
snapshot: Dict[str, str] = {}
for protected in self.PROTECTED_PATHS:
target = self.plugin_root / protected
if target.is_file():
rel = protected
snapshot[rel] = self._hash_file(target)
elif target.is_dir():
for f in sorted(target.rglob('*')):
if f.is_file():
rel = str(f.relative_to(self.plugin_root))
snapshot[rel] = self._hash_file(f)
logger.info(f"🔒 Protection snapshot: {len(snapshot)} Plugin-owned files hashed")
return snapshot
def _validate_protected_files(self, snapshot: Dict[str, str]) -> None:
"""
Re-hash every file from the snapshot and compare.
Called AFTER sync to verify no protected file was touched.
Raises:
ProtectionViolationError: if any protected file was modified or deleted.
"""
violations: List[str] = []
for rel_path, original_hash in snapshot.items():
current = self.plugin_root / rel_path
if not current.exists():
violations.append(f"DELETED : {rel_path}")
else:
current_hash = self._hash_file(current)
if current_hash != original_hash:
violations.append(f"MODIFIED : {rel_path}")
if violations:
msg = (
"🚨 PROTECTION VIOLATION — sync modified Plugin-owned files:\n"
+ "\n".join(f"{v}" for v in violations)
+ "\n\nFix: ensure SYNC_MAPPINGS does not target any path in PROTECTED_PATHS."
)
logger.error(msg)
raise ProtectionViolationError(msg)
logger.info(f"🔒 Protection check passed — {len(snapshot)} Plugin-owned files unchanged")
# ── Core sync workflow ─────────────────────────────────────────────────────
def _clone_framework(self) -> Path:
"""Clone Framework repository to temp directory."""
logger.info(f"📥 Cloning Framework: {self.framework_repo}")
self.temp_dir = tempfile.mkdtemp(prefix='superclaude_framework_')
framework_path = Path(self.temp_dir) / 'framework'
try:
subprocess.run(
['git', 'clone', '--depth', '1', self.framework_repo, str(framework_path)],
check=True,
capture_output=True,
text=True
)
logger.info(f"✅ Cloned to: {framework_path}")
return framework_path
except subprocess.CalledProcessError as e:
logger.error(f"Failed to clone Framework: {e.stderr}")
raise
def _get_commit_hash(self, repo_path: Path) -> str:
"""Get current commit hash from repository."""
try:
result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
cwd=repo_path,
check=True,
capture_output=True,
text=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return "unknown"
def _get_version(self, framework_path: Path) -> str:
"""Extract version from Framework."""
# Try to read version from plugin.json or package.json
for version_file in ['plugin.json', 'package.json']:
version_path = framework_path / version_file
if version_path.exists():
try:
data = json.loads(version_path.read_text())
if 'version' in data:
return data['version']
except (json.JSONDecodeError, KeyError):
continue
# Fallback to current Plugin version
plugin_json = self.plugin_root / 'plugin.json'
if plugin_json.exists():
try:
data = json.loads(plugin_json.read_text())
return data.get('version', '1.0.0')
except json.JSONDecodeError:
pass
return '1.0.0'
def _create_backup(self):
"""Create backup of current plugin state."""
logger.info("📦 Creating backup...")
mcp_merger = McpMerger(self.plugin_root)
backup_path = mcp_merger.backup_current()
if backup_path:
logger.info(f"✅ Backup created: {backup_path}")
def _sync_content(self, framework_path: Path) -> Dict[str, int]:
"""Sync and transform content from Framework."""
logger.info("🔄 Syncing content...")
file_syncer = FileSyncer(self.plugin_root, self.dry_run)
stats = {
'files_synced': 0,
'files_modified': 0,
'commands': 0,
'agents': 0
}
# Sync commands with transformation
logger.info("📝 Syncing commands...")
source_commands = framework_path / 'src/superclaude/commands'
dest_commands = self.plugin_root / 'commands'
if source_commands.exists():
cmd_stats = file_syncer.sync_directory(
source_commands,
dest_commands,
filename_prefix='sc-',
transform_fn=ContentTransformer.transform_command
)
stats['commands'] = cmd_stats['synced'] + cmd_stats['modified']
stats['files_synced'] += cmd_stats['synced']
stats['files_modified'] += cmd_stats['modified']
logger.info(f"✅ Commands: {stats['commands']} transformed")
# Sync agents with transformation
logger.info("📝 Syncing agents...")
source_agents = framework_path / 'src/superclaude/agents'
dest_agents = self.plugin_root / 'agents'
if source_agents.exists():
agent_stats = file_syncer.sync_directory(
source_agents,
dest_agents,
filename_prefix='sc-',
transform_fn=ContentTransformer.transform_agent
)
stats['agents'] = agent_stats['synced'] + agent_stats['modified']
stats['files_synced'] += agent_stats['synced']
stats['files_modified'] += agent_stats['modified']
logger.info(f"✅ Agents: {stats['agents']} transformed")
# core/ and modes/ are in PROTECTED_PATHS — Plugin maintains its own versions.
# They are intentionally excluded from SYNC_MAPPINGS and will never be
# overwritten here. To re-enable Framework sync for either directory,
# remove it from PROTECTED_PATHS and add it back to SYNC_MAPPINGS.
logger.info("🔒 core/ and modes/ are Plugin-owned (PROTECTED_PATHS) — skipping")
return stats
def _generate_plugin_json(self, framework_version: str):
"""Generate plugin.json from synced commands."""
logger.info("📄 Generating plugin.json...")
generator = PluginJsonGenerator(self.plugin_root)
plugin_json = generator.generate(framework_version)
generator.write(plugin_json, self.dry_run)
def _merge_mcp_configs(self, framework_path: Path) -> int:
"""Merge MCP configurations from Framework."""
logger.info("🔗 Merging MCP configurations...")
# Read Framework MCP config
framework_plugin_json = framework_path / 'plugin.json'
framework_mcp = {}
if framework_plugin_json.exists():
try:
data = json.loads(framework_plugin_json.read_text())
framework_mcp = data.get('mcpServers', {})
except json.JSONDecodeError:
logger.warning("Failed to read Framework plugin.json")
# Read Plugin MCP config
plugin_json_path = self.plugin_root / 'plugin.json'
plugin_mcp = {}
if plugin_json_path.exists():
try:
data = json.loads(plugin_json_path.read_text())
plugin_mcp = data.get('mcpServers', {})
except json.JSONDecodeError:
logger.warning("Failed to read Plugin plugin.json")
# Merge configurations
merger = McpMerger(self.plugin_root)
merged_mcp, warnings = merger.merge(framework_mcp, plugin_mcp)
# Log warnings
for warning in warnings:
logger.warning(f"⚠️ {warning}")
self.warnings.append(warning)
# Update plugin.json with merged MCP config
if not self.dry_run and plugin_json_path.exists():
data = json.loads(plugin_json_path.read_text())
data['mcpServers'] = merged_mcp
plugin_json_path.write_text(
json.dumps(data, indent=2) + '\n',
encoding='utf-8'
)
logger.info(f"✅ MCP servers merged: {len(merged_mcp)}")
return len(merged_mcp)
def _validate_sync(self):
"""Validate sync results."""
logger.info("🔍 Validating sync...")
# Check commands directory
commands_dir = self.plugin_root / 'commands'
if commands_dir.exists():
sc_commands = list(commands_dir.glob('sc-*.md'))
logger.info(f"✅ Found {len(sc_commands)} sc- prefixed commands")
# Check agents directory
agents_dir = self.plugin_root / 'agents'
if agents_dir.exists():
sc_agents = list(agents_dir.glob('sc-*.md'))
logger.info(f"✅ Found {len(sc_agents)} sc- prefixed agents")
# Check plugin.json
plugin_json_path = self.plugin_root / '.claude-plugin' / 'plugin.json'
if plugin_json_path.exists():
logger.info(f"✅ plugin.json exists at {plugin_json_path}")
else:
logger.warning("⚠️ plugin.json not found")
def _cleanup(self):
"""Clean up temporary directories."""
if self.temp_dir and Path(self.temp_dir).exists():
shutil.rmtree(self.temp_dir)
logger.debug(f"🧹 Cleaned up temp directory: {self.temp_dir}")
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Sync SuperClaude Framework to Plugin with namespace isolation'
)
parser.add_argument(
'--framework-repo',
default='https://github.com/SuperClaude-Org/SuperClaude_Framework',
help='Framework repository URL'
)
parser.add_argument(
'--plugin-root',
type=Path,
default=Path.cwd(),
help='Plugin repository root path'
)
parser.add_argument(
'--dry-run',
type=lambda x: x.lower() in ('true', '1', 'yes'),
default=False,
help='Preview changes without applying'
)
parser.add_argument(
'--output-report',
type=Path,
help='Save sync report to file'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Enable verbose logging'
)
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
if args.dry_run:
logger.info("🔍 DRY RUN MODE - No changes will be applied")
# Run sync
syncer = FrameworkSyncer(
framework_repo=args.framework_repo,
plugin_root=args.plugin_root,
dry_run=args.dry_run
)
result = syncer.sync()
# Output report
if args.output_report:
args.output_report.write_text(
json.dumps(result.to_dict(), indent=2) + '\n'
)
logger.info(f"📊 Report saved to: {args.output_report}")
# Print summary
print("\n" + "=" * 60)
print("SYNC SUMMARY")
print("=" * 60)
print(f"Success: {result.success}")
print(f"Framework Version: {result.framework_version}")
print(f"Framework Commit: {result.framework_commit[:8]}")
print(f"Files Synced: {result.files_synced}")
print(f"Files Modified: {result.files_modified}")
print(f"Commands Transformed: {result.commands_transformed}")
print(f"Agents Transformed: {result.agents_transformed}")
print(f"MCP Servers Merged: {result.mcp_servers_merged}")
if result.warnings:
print(f"\n⚠️ Warnings: {len(result.warnings)}")
for warning in result.warnings:
print(f" - {warning}")
if result.errors:
print(f"\n❌ Errors: {len(result.errors)}")
for error in result.errors:
print(f" - {error}")
print("=" * 60)
sys.exit(0 if result.success else 1)
if __name__ == '__main__':
main()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# SuperClaude Legacy Cleanup Script
# Removes old SuperClaude-related files from ~/.claude
#
set -euo pipefail
CLAUDE_DIR="$HOME/.claude"
BACKUP_DIR="$HOME/.claude-superclaude-backup-$(date +%Y%m%d_%H%M%S)"
echo "🧹 SuperClaude Legacy Cleanup"
echo "=============================="
echo ""
# Check if .claude directory exists
if [[ ! -d "$CLAUDE_DIR" ]]; then
echo "✅ No .claude directory found - nothing to clean"
exit 0
fi
# List of SuperClaude-related files/directories to remove
SUPERCLAUDE_ITEMS=(
# SuperClaude plugin (if exists)
"plugins/superclaude@superclaude"
# Legacy SuperClaude configs (if any)
"superclaude.json"
"superclaude_config.json"
)
# Function to backup and remove
backup_and_remove() {
local item="$1"
local full_path="$CLAUDE_DIR/$item"
if [[ -e "$full_path" ]] || [[ -L "$full_path" ]]; then
echo "📦 Backing up: $item"
mkdir -p "$BACKUP_DIR/$(dirname "$item")"
cp -R "$full_path" "$BACKUP_DIR/$item" 2>/dev/null || true
echo "🗑️ Removing: $item"
rm -rf "$full_path"
return 0
fi
return 1
}
echo "🔍 Scanning for SuperClaude files..."
echo ""
FOUND_COUNT=0
for item in "${SUPERCLAUDE_ITEMS[@]}"; do
if backup_and_remove "$item"; then
((FOUND_COUNT++))
fi
done
# Clean up settings.json if it contains SuperClaude plugin reference
SETTINGS_FILE="$CLAUDE_DIR/settings.json"
if [[ -f "$SETTINGS_FILE" ]]; then
if grep -q "superclaude@superclaude" "$SETTINGS_FILE" 2>/dev/null; then
echo "📦 Backing up: settings.json"
mkdir -p "$BACKUP_DIR"
cp "$SETTINGS_FILE" "$BACKUP_DIR/settings.json"
echo "🧹 Removing SuperClaude plugin from settings.json"
# Use Python to properly manipulate JSON
python3 - <<'PYEOF' "$SETTINGS_FILE"
import json
import sys
settings_file = sys.argv[1]
try:
with open(settings_file, 'r') as f:
settings = json.load(f)
# Remove SuperClaude plugin
if 'enabledPlugins' in settings:
if 'superclaude@superclaude' in settings['enabledPlugins']:
del settings['enabledPlugins']['superclaude@superclaude']
print(f" ✅ Removed superclaude@superclaude from enabledPlugins")
# Remove enabledPlugins if empty
if not settings['enabledPlugins']:
del settings['enabledPlugins']
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
f.write('\n')
except Exception as e:
print(f" ⚠️ Failed to update settings.json: {e}", file=sys.stderr)
sys.exit(1)
PYEOF
((FOUND_COUNT++))
fi
fi
echo ""
echo "=============================="
if [[ $FOUND_COUNT -eq 0 ]]; then
echo "✅ No SuperClaude files found - already clean!"
rmdir "$BACKUP_DIR" 2>/dev/null || true
else
echo "🎉 Cleanup complete!"
echo ""
echo "📦 Backup saved to:"
echo " $BACKUP_DIR"
echo ""
echo "Removed $FOUND_COUNT SuperClaude-related item(s)"
fi
echo ""
echo "️ Note: Official Claude Code files were NOT touched"
echo " (history.jsonl, mcp.json, projects/, etc.)"