Initial commit
This commit is contained in:
205
skills/scripts/changelog_generator.py
Normal file
205
skills/scripts/changelog_generator.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CHANGELOG generator from git commit history.
|
||||
Supports Conventional Commits format.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ChangelogGenerator:
|
||||
def __init__(self, repo_path: str = "."):
|
||||
self.repo_path = Path(repo_path).resolve()
|
||||
|
||||
# Conventional Commits types
|
||||
self.commit_types = {
|
||||
'feat': 'Added',
|
||||
'fix': 'Fixed',
|
||||
'docs': 'Documentation',
|
||||
'style': 'Style',
|
||||
'refactor': 'Changed',
|
||||
'perf': 'Performance',
|
||||
'test': 'Tests',
|
||||
'chore': 'Chore',
|
||||
'build': 'Build',
|
||||
'ci': 'CI/CD',
|
||||
}
|
||||
|
||||
def get_commits_since_tag(
|
||||
self,
|
||||
since_tag: Optional[str] = None) -> List[str]:
|
||||
"""Get commit messages since last tag."""
|
||||
try:
|
||||
if since_tag:
|
||||
cmd = [
|
||||
'git',
|
||||
'log',
|
||||
f'{since_tag}..HEAD',
|
||||
'--pretty=format:%s']
|
||||
else:
|
||||
# Get all commits if no tag specified
|
||||
cmd = ['git', 'log', '--pretty=format:%s']
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
return (
|
||||
result.stdout.strip().split('\n')
|
||||
if result.stdout.strip() else []
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error getting git commits: {e.stderr}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
def parse_commit(self, commit_message: str) -> Dict[str, str]:
|
||||
"""Parse conventional commit message."""
|
||||
# Pattern: type(scope): description
|
||||
pattern = r'^(\w+)(?:\(([^)]+)\))?: (.+)$'
|
||||
match = re.match(pattern, commit_message)
|
||||
|
||||
if match:
|
||||
commit_type, scope, description = match.groups()
|
||||
return {
|
||||
'type': commit_type,
|
||||
'scope': scope or '',
|
||||
'description': description,
|
||||
'raw': commit_message
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'type': 'other',
|
||||
'scope': '',
|
||||
'description': commit_message,
|
||||
'raw': commit_message
|
||||
}
|
||||
|
||||
def group_commits(self, commits: List[str]) -> Dict[str, List[str]]:
|
||||
"""Group commits by type."""
|
||||
grouped: Dict[str, List[str]] = {}
|
||||
|
||||
for commit_msg in commits:
|
||||
parsed = self.parse_commit(commit_msg)
|
||||
commit_type = parsed['type']
|
||||
description = parsed['description']
|
||||
|
||||
category = self.commit_types.get(commit_type, 'Other')
|
||||
|
||||
if category not in grouped:
|
||||
grouped[category] = []
|
||||
|
||||
grouped[category].append(description)
|
||||
|
||||
return grouped
|
||||
|
||||
def generate_changelog_entry(
|
||||
self,
|
||||
version: str,
|
||||
since_tag: Optional[str] = None) -> str:
|
||||
"""Generate changelog entry for new version."""
|
||||
commits = self.get_commits_since_tag(since_tag)
|
||||
|
||||
if not commits:
|
||||
print("No commits found", file=sys.stderr)
|
||||
return ""
|
||||
|
||||
grouped = self.group_commits(commits)
|
||||
|
||||
# Build changelog entry
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
lines = [
|
||||
f"## [{version}] - {date}",
|
||||
""
|
||||
]
|
||||
|
||||
# Add sections in order
|
||||
section_order = [
|
||||
'Added', 'Fixed', 'Changed', 'Deprecated', 'Removed',
|
||||
'Security', 'Performance', 'Documentation', 'Other'
|
||||
]
|
||||
|
||||
for section in section_order:
|
||||
if section in grouped:
|
||||
lines.append(f"### {section}")
|
||||
for item in grouped[section]:
|
||||
lines.append(f"- {item}")
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def update_changelog_file(
|
||||
self,
|
||||
version: str,
|
||||
since_tag: Optional[str] = None,
|
||||
changelog_path: str = "CHANGELOG.md"
|
||||
) -> bool:
|
||||
"""Update CHANGELOG.md file with new entry."""
|
||||
entry = self.generate_changelog_entry(version, since_tag)
|
||||
|
||||
if not entry:
|
||||
return False
|
||||
|
||||
changelog_file = self.repo_path / changelog_path
|
||||
|
||||
if changelog_file.exists():
|
||||
# Insert at top after header
|
||||
content = changelog_file.read_text(encoding='utf-8')
|
||||
|
||||
# Find position to insert (after # Changelog header)
|
||||
lines = content.split('\n')
|
||||
insert_pos = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('# '):
|
||||
insert_pos = i + 1
|
||||
# Skip empty lines after header
|
||||
while (insert_pos < len(lines) and
|
||||
not lines[insert_pos].strip()):
|
||||
insert_pos += 1
|
||||
break
|
||||
|
||||
lines.insert(insert_pos, entry)
|
||||
new_content = '\n'.join(lines)
|
||||
|
||||
else:
|
||||
# Create new CHANGELOG
|
||||
new_content = f"# Changelog\n\n{entry}"
|
||||
|
||||
changelog_file.write_text(new_content, encoding='utf-8')
|
||||
print(f"✓ Updated {changelog_path}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
"Usage: python changelog_generator.py "
|
||||
"<version> [since_tag] [changelog_path]",
|
||||
file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
version = sys.argv[1]
|
||||
since_tag = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
changelog_path = sys.argv[3] if len(sys.argv) > 3 else "CHANGELOG.md"
|
||||
|
||||
generator = ChangelogGenerator()
|
||||
|
||||
if not generator.update_changelog_file(version, since_tag, changelog_path):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
281
skills/scripts/detect_project.py
Normal file
281
skills/scripts/detect_project.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Detect project type and version files automatically.
|
||||
Supports: Node.js, Python, Rust, Go, Unity, Unreal Engine,
|
||||
Claude Code Plugins, etc.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tomllib # Python 3.11+ required
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# Check Python version
|
||||
if sys.version_info < (3, 11):
|
||||
print(
|
||||
"Error: Python 3.11+ is required for this script",
|
||||
file=sys.stderr
|
||||
)
|
||||
print(
|
||||
"Current version: "
|
||||
f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ProjectDetector:
|
||||
def __init__(self, project_root: str = "."):
|
||||
self.root = Path(project_root).resolve()
|
||||
|
||||
def detect(self) -> Dict[str, Any]:
|
||||
"""Detect project type and return version file paths."""
|
||||
result: Dict[str, Any] = {
|
||||
"project_type": None,
|
||||
"version_files": [],
|
||||
"detected_version": None
|
||||
}
|
||||
|
||||
# Check in priority order
|
||||
detectors = [
|
||||
self._detect_unity,
|
||||
self._detect_unreal,
|
||||
self._detect_nodejs,
|
||||
self._detect_python,
|
||||
self._detect_rust,
|
||||
self._detect_go,
|
||||
self._detect_claude_plugin,
|
||||
self._detect_generic
|
||||
]
|
||||
|
||||
for detector in detectors:
|
||||
detection = detector()
|
||||
if detection:
|
||||
result.update(detection)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def _detect_unity(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Unity project."""
|
||||
project_settings = (
|
||||
self.root / "ProjectSettings" / "ProjectSettings.asset"
|
||||
)
|
||||
version_json = self.root / "version.json"
|
||||
|
||||
if project_settings.exists():
|
||||
files = [str(project_settings)]
|
||||
if version_json.exists():
|
||||
files.append(str(version_json))
|
||||
|
||||
# Try to read version from version.json first
|
||||
version = None
|
||||
if version_json.exists():
|
||||
try:
|
||||
data = json.loads(
|
||||
version_json.read_text(encoding='utf-8')
|
||||
)
|
||||
version = data.get('version')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to ProjectSettings.asset
|
||||
if not version:
|
||||
try:
|
||||
content = project_settings.read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
for line in content.split('\n'):
|
||||
if 'bundleVersion:' in line:
|
||||
version = line.split(':', 1)[1].strip()
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_type": "unity",
|
||||
"version_files": files,
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_unreal(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Unreal Engine project."""
|
||||
uproject_files = list(self.root.glob("*.uproject"))
|
||||
|
||||
if uproject_files:
|
||||
uproject = uproject_files[0]
|
||||
version = None
|
||||
|
||||
try:
|
||||
data = json.loads(uproject.read_text(encoding='utf-8'))
|
||||
version = data.get('Version') or data.get('EngineAssociation')
|
||||
except (json.JSONDecodeError, IOError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_type": "unreal",
|
||||
"version_files": [str(uproject)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_nodejs(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Node.js project."""
|
||||
package_json = self.root / "package.json"
|
||||
|
||||
if package_json.exists():
|
||||
version = None
|
||||
|
||||
try:
|
||||
data = json.loads(package_json.read_text(encoding='utf-8'))
|
||||
version = data.get('version')
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_type": "nodejs",
|
||||
"version_files": [str(package_json)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_python(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Python project."""
|
||||
pyproject = self.root / "pyproject.toml"
|
||||
setup_py = self.root / "setup.py"
|
||||
|
||||
if pyproject.exists():
|
||||
version = None
|
||||
|
||||
if tomllib:
|
||||
try:
|
||||
data = tomllib.loads(
|
||||
pyproject.read_text(encoding='utf-8')
|
||||
)
|
||||
version = (
|
||||
data.get('project', {}).get('version') or
|
||||
data.get('tool', {}).get('poetry', {}).get('version')
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_type": "python",
|
||||
"version_files": [str(pyproject)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
elif setup_py.exists():
|
||||
return {
|
||||
"project_type": "python",
|
||||
"version_files": [str(setup_py)],
|
||||
"detected_version": None # Requires parsing Python
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_rust(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Rust project."""
|
||||
cargo_toml = self.root / "Cargo.toml"
|
||||
|
||||
if cargo_toml.exists():
|
||||
version = None
|
||||
|
||||
if tomllib:
|
||||
try:
|
||||
data = tomllib.loads(
|
||||
cargo_toml.read_text(
|
||||
encoding='utf-8'))
|
||||
version = data.get('package', {}).get('version')
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_type": "rust",
|
||||
"version_files": [str(cargo_toml)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_go(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Go project."""
|
||||
go_mod = self.root / "go.mod"
|
||||
|
||||
if go_mod.exists():
|
||||
# Go doesn't have built-in versioning in go.mod
|
||||
# Check for common version files
|
||||
version_file = self.root / "VERSION"
|
||||
|
||||
if version_file.exists():
|
||||
version = version_file.read_text().strip()
|
||||
return {
|
||||
"project_type": "go",
|
||||
"version_files": [str(version_file)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return {
|
||||
"project_type": "go",
|
||||
"version_files": [],
|
||||
"detected_version": None
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_claude_plugin(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect Claude Code plugin."""
|
||||
plugin_json = self.root / ".claude-plugin" / "plugin.json"
|
||||
|
||||
if plugin_json.exists():
|
||||
version = None
|
||||
|
||||
try:
|
||||
data = json.loads(plugin_json.read_text(encoding='utf-8'))
|
||||
version = data.get('version')
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_type": "claude-plugin",
|
||||
"version_files": [str(plugin_json)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _detect_generic(self) -> Optional[Dict[str, Any]]:
|
||||
"""Detect generic project with VERSION file."""
|
||||
version_file = self.root / "VERSION"
|
||||
|
||||
if version_file.exists():
|
||||
version = version_file.read_text().strip()
|
||||
return {
|
||||
"project_type": "generic",
|
||||
"version_files": [str(version_file)],
|
||||
"detected_version": version
|
||||
}
|
||||
|
||||
return {
|
||||
"project_type": "unknown",
|
||||
"version_files": [],
|
||||
"detected_version": None
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
project_root = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
|
||||
detector = ProjectDetector(project_root)
|
||||
result = detector.detect()
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
154
skills/scripts/git_operations.py
Normal file
154
skills/scripts/git_operations.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Git operations wrapper with cross-platform support.
|
||||
Handles: commit, tag, push, branch detection
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class GitOperations:
|
||||
def __init__(self, repo_path: str = "."):
|
||||
self.repo_path = Path(repo_path).resolve()
|
||||
|
||||
def run_command(
|
||||
self,
|
||||
cmd: List[str],
|
||||
check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
"""Run git command with proper error handling."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
encoding='utf-8'
|
||||
)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Git command failed: {' '.join(cmd)}", file=sys.stderr)
|
||||
print(f"Error: {e.stderr}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
def get_current_branch(self) -> str:
|
||||
"""Get current git branch name."""
|
||||
result = self.run_command(['git', 'branch', '--show-current'])
|
||||
return result.stdout.strip()
|
||||
|
||||
def get_status(self) -> str:
|
||||
"""Get git status."""
|
||||
result = self.run_command(['git', 'status', '--porcelain'])
|
||||
return result.stdout
|
||||
|
||||
def add_files(self, files: Optional[List[str]] = None) -> bool:
|
||||
"""Stage files for commit."""
|
||||
if files:
|
||||
self.run_command(['git', 'add'] + files)
|
||||
else:
|
||||
self.run_command(['git', 'add', '-A'])
|
||||
|
||||
print("✓ Files staged for commit")
|
||||
return True
|
||||
|
||||
def commit(self, message: str) -> bool:
|
||||
"""Create git commit."""
|
||||
self.run_command(['git', 'commit', '-m', message])
|
||||
print(f"✓ Committed: {message.split('\n')[0][:60]}...")
|
||||
return True
|
||||
|
||||
def create_tag(self, tag_name: str, message: Optional[str] = None) -> bool:
|
||||
"""Create annotated git tag."""
|
||||
cmd = ['git', 'tag', '-a', tag_name]
|
||||
|
||||
if message:
|
||||
cmd.extend(['-m', message])
|
||||
else:
|
||||
cmd.extend(['-m', f"Release {tag_name}"])
|
||||
|
||||
self.run_command(cmd)
|
||||
print(f"✓ Created tag: {tag_name}")
|
||||
return True
|
||||
|
||||
def push(
|
||||
self,
|
||||
remote: str = 'origin',
|
||||
branch: Optional[str] = None,
|
||||
tags: bool = False) -> bool:
|
||||
"""Push commits and/or tags to remote."""
|
||||
if tags:
|
||||
# Push tags
|
||||
cmd = ['git', 'push', remote, '--tags']
|
||||
self.run_command(cmd)
|
||||
print(f"✓ Pushed tags to {remote}")
|
||||
else:
|
||||
# Push branch
|
||||
if not branch:
|
||||
branch = self.get_current_branch()
|
||||
|
||||
cmd = ['git', 'push', remote, branch]
|
||||
self.run_command(cmd)
|
||||
print(f"✓ Pushed {branch} to {remote}")
|
||||
|
||||
return True
|
||||
|
||||
def tag_exists(self, tag_name: str) -> bool:
|
||||
"""Check if tag exists."""
|
||||
result = self.run_command(['git', 'tag', '-l', tag_name], check=False)
|
||||
return bool(result.stdout.strip())
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:", file=sys.stderr)
|
||||
print(" python git_operations.py commit <message>", file=sys.stderr)
|
||||
print(
|
||||
" python git_operations.py tag <tag_name> [message]",
|
||||
file=sys.stderr)
|
||||
print(" python git_operations.py push [branch]", file=sys.stderr)
|
||||
print(" python git_operations.py push-tags", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
git = GitOperations()
|
||||
|
||||
try:
|
||||
if action == 'commit':
|
||||
if len(sys.argv) < 3:
|
||||
print("Error: Commit message required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
message = sys.argv[2]
|
||||
git.add_files()
|
||||
git.commit(message)
|
||||
|
||||
elif action == 'tag':
|
||||
if len(sys.argv) < 3:
|
||||
print("Error: Tag name required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tag_name = sys.argv[2]
|
||||
message = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
git.create_tag(tag_name, message)
|
||||
|
||||
elif action == 'push':
|
||||
branch = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
git.push(branch=branch)
|
||||
|
||||
elif action == 'push-tags':
|
||||
git.push(tags=True)
|
||||
|
||||
else:
|
||||
print(f"Error: Unknown action '{action}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
skills/scripts/sync_unity_version.py
Normal file
87
skills/scripts/sync_unity_version.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sync Unity version from version.json to ProjectSettings.asset.
|
||||
This ensures version.json is the single source of truth.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sync_unity_version(project_root: str = ".") -> bool:
|
||||
"""Sync version from version.json to ProjectSettings.asset."""
|
||||
root = Path(project_root).resolve()
|
||||
|
||||
version_json_path = root / "version.json"
|
||||
project_settings_path = root / "ProjectSettings" / "ProjectSettings.asset"
|
||||
|
||||
# Check if files exist
|
||||
if not version_json_path.exists():
|
||||
print(
|
||||
f"Error: version.json not found at {version_json_path}",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
if not project_settings_path.exists():
|
||||
print(
|
||||
f"Error: ProjectSettings.asset not found at "
|
||||
f"{project_settings_path}",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Read version from version.json
|
||||
version_data = json.loads(
|
||||
version_json_path.read_text(
|
||||
encoding='utf-8'))
|
||||
version = version_data.get('version')
|
||||
|
||||
if not version:
|
||||
print(
|
||||
"Error: 'version' field not found in version.json",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Read ProjectSettings.asset
|
||||
content = project_settings_path.read_text(encoding='utf-8')
|
||||
|
||||
# Find and replace bundleVersion
|
||||
pattern = r'(bundleVersion:\s*)(.+)'
|
||||
replacement = r'\g<1>' + version
|
||||
|
||||
if not re.search(pattern, content):
|
||||
print(
|
||||
"Error: bundleVersion not found in ProjectSettings.asset",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Update content
|
||||
new_content = re.sub(pattern, replacement, content)
|
||||
|
||||
if new_content == content:
|
||||
print(f"✓ Version already synced: {version}")
|
||||
return True
|
||||
|
||||
# Write back
|
||||
project_settings_path.write_text(new_content, encoding='utf-8')
|
||||
print(f"✓ Synced Unity version: {version}")
|
||||
print(" version.json → ProjectSettings.asset")
|
||||
|
||||
return True
|
||||
|
||||
except (json.JSONDecodeError, IOError, UnicodeDecodeError) as e:
|
||||
print(f"Error syncing Unity version: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
project_root = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
|
||||
if not sync_unity_version(project_root):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
231
skills/scripts/update_version.py
Normal file
231
skills/scripts/update_version.py
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Universal version updater supporting multiple file formats.
|
||||
Supports: JSON, TOML, YAML, plain text, Unity asset files, etc.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
class VersionUpdater:
|
||||
def __init__(self):
|
||||
self.updated_files: List[str] = []
|
||||
|
||||
def update(self, file_path: str, new_version: str) -> bool:
|
||||
"""Update version in file based on its format."""
|
||||
path = Path(file_path)
|
||||
|
||||
if not path.exists():
|
||||
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Detect file type and update accordingly
|
||||
if path.suffix == '.json':
|
||||
return self._update_json(path, new_version)
|
||||
elif path.suffix == '.toml':
|
||||
return self._update_toml(path, new_version)
|
||||
elif path.suffix in ['.yaml', '.yml']:
|
||||
return self._update_yaml(path, new_version)
|
||||
elif path.name == 'ProjectSettings.asset':
|
||||
return self._update_unity_asset(path, new_version)
|
||||
elif path.suffix == '.uproject':
|
||||
return self._update_unreal_uproject(path, new_version)
|
||||
elif path.name in ['VERSION', 'version.txt']:
|
||||
return self._update_plain_text(path, new_version)
|
||||
else:
|
||||
print(f"Warning: Unknown file type: {file_path}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_json(self, path: Path, new_version: str) -> bool:
|
||||
"""Update version in JSON file."""
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
# Update version field
|
||||
if 'version' in data:
|
||||
old_version = data['version']
|
||||
data['version'] = new_version
|
||||
print(f"Updated {path.name}: {old_version} → {new_version}")
|
||||
else:
|
||||
data['version'] = new_version
|
||||
print(f"Added version to {path.name}: {new_version}")
|
||||
|
||||
# Write back with proper formatting
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
data,
|
||||
indent=2,
|
||||
ensure_ascii=False) +
|
||||
'\n',
|
||||
encoding='utf-8')
|
||||
self.updated_files.append(str(path))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating {path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_toml(self, path: Path, new_version: str) -> bool:
|
||||
"""Update version in TOML file."""
|
||||
try:
|
||||
content = path.read_text(encoding='utf-8')
|
||||
|
||||
# Try to find and replace version
|
||||
patterns = [
|
||||
(r'(version\s*=\s*["\'])([^"\']+)(["\'])',
|
||||
r'\g<1>' + new_version + r'\g<3>'),
|
||||
(r'(\[project\].*?version\s*=\s*["\'])([^"\']+)(["\'])',
|
||||
r'\g<1>' + new_version + r'\g<3>'),
|
||||
]
|
||||
|
||||
updated = False
|
||||
for pattern, replacement in patterns:
|
||||
if re.search(pattern, content, re.DOTALL):
|
||||
content = re.sub(
|
||||
pattern, replacement, content, flags=re.DOTALL)
|
||||
updated = True
|
||||
break
|
||||
|
||||
if updated:
|
||||
path.write_text(content, encoding='utf-8')
|
||||
self.updated_files.append(str(path))
|
||||
print(f"Updated {path.name} → {new_version}")
|
||||
return True
|
||||
else:
|
||||
print(
|
||||
f"Warning: Could not find version in {
|
||||
path.name}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating {path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_yaml(self, path: Path, new_version: str) -> bool:
|
||||
"""Update version in YAML file."""
|
||||
try:
|
||||
content = path.read_text(encoding='utf-8')
|
||||
|
||||
# Replace version field
|
||||
pattern = r'(version:\s*["\']?)([^"\'\n]+)(["\']?)'
|
||||
replacement = r'\g<1>' + new_version + r'\g<3>'
|
||||
|
||||
if re.search(pattern, content):
|
||||
content = re.sub(pattern, replacement, content)
|
||||
path.write_text(content, encoding='utf-8')
|
||||
self.updated_files.append(str(path))
|
||||
print(f"Updated {path.name} → {new_version}")
|
||||
return True
|
||||
else:
|
||||
print(
|
||||
f"Warning: Could not find version in {
|
||||
path.name}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating {path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_unity_asset(self, path: Path, new_version: str) -> bool:
|
||||
"""Update bundleVersion in Unity ProjectSettings.asset."""
|
||||
try:
|
||||
content = path.read_text(encoding='utf-8')
|
||||
|
||||
# Replace bundleVersion
|
||||
pattern = r'(bundleVersion:\s*)(.+)'
|
||||
replacement = r'\g<1>' + new_version
|
||||
|
||||
if re.search(pattern, content):
|
||||
old_content = content
|
||||
content = re.sub(pattern, replacement, content)
|
||||
|
||||
if content != old_content:
|
||||
path.write_text(content, encoding='utf-8')
|
||||
self.updated_files.append(str(path))
|
||||
print(
|
||||
f"Updated Unity ProjectSettings.asset → {new_version}")
|
||||
return True
|
||||
|
||||
print(
|
||||
f"Warning: Could not find bundleVersion in {
|
||||
path.name}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating {path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_unreal_uproject(self, path: Path, new_version: str) -> bool:
|
||||
"""Update version in Unreal .uproject file."""
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
# Unreal uses EngineAssociation or custom Version field
|
||||
if 'Version' in data:
|
||||
old_version = data['Version']
|
||||
data['Version'] = new_version
|
||||
print(f"Updated {path.name}: {old_version} → {new_version}")
|
||||
else:
|
||||
data['Version'] = new_version
|
||||
print(f"Added Version to {path.name}: {new_version}")
|
||||
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
data,
|
||||
indent=4,
|
||||
ensure_ascii=False) +
|
||||
'\n',
|
||||
encoding='utf-8')
|
||||
self.updated_files.append(str(path))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating {path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_plain_text(self, path: Path, new_version: str) -> bool:
|
||||
"""Update plain text version file."""
|
||||
try:
|
||||
path.write_text(new_version + '\n', encoding='utf-8')
|
||||
self.updated_files.append(str(path))
|
||||
print(f"Updated {path.name} → {new_version}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating {path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(
|
||||
"Usage: python update_version.py <file_path> <new_version>",
|
||||
file=sys.stderr)
|
||||
print(
|
||||
" python update_version.py "
|
||||
"<file1> <file2> ... <new_version>",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
files = sys.argv[1:-1]
|
||||
new_version = sys.argv[-1]
|
||||
|
||||
updater = VersionUpdater()
|
||||
success_count = 0
|
||||
|
||||
for file_path in files:
|
||||
if updater.update(file_path, new_version):
|
||||
success_count += 1
|
||||
|
||||
print(f"\nUpdated {success_count}/{len(files)} files successfully")
|
||||
|
||||
if success_count < len(files):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user