Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "auto-release-manager",
|
||||||
|
"description": "Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.) with intelligent project detection and cross-platform support.",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"author": {
|
||||||
|
"name": "Dev GOM",
|
||||||
|
"url": "https://github.com/Dev-GOM/claude-code-marketplace"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# auto-release-manager
|
||||||
|
|
||||||
|
Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.) with intelligent project detection and cross-platform support.
|
||||||
85
plugin.lock.json
Normal file
85
plugin.lock.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:Dev-GOM/claude-code-marketplace:plugins/auto-release-manager",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "c7a9b94dd277fb271aedc0f56f97155072bf9419",
|
||||||
|
"treeHash": "73fb86e7061159773e5f4a0f1b0b600e79d7abbf71d4fc7ce8370cce46b3f608",
|
||||||
|
"generatedAt": "2025-11-28T10:10:17.061929Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "auto-release-manager",
|
||||||
|
"description": "Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.) with intelligent project detection and cross-platform support.",
|
||||||
|
"version": "1.0.3"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "a5ae49a6fe1b46791ade1d36417b65cd4137c43b06edd526bf49d99d88c5e39c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "377857f3b2365fc23cc778325e12a27d957aae117866fd66ade504159238ce74"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/SKILL.md",
|
||||||
|
"sha256": "5869f71f5e65b30e9febe9ed3f3d19d107c31636b28923d45ed984c8a63c8bd6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/references/unreal-guide.md",
|
||||||
|
"sha256": "b6b1d1b8980bb4aebb45395094def41254c383b27385546edf39c7a8d34ff197"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/references/project-types.md",
|
||||||
|
"sha256": "56641ef5cbd333d4579527386eac403d351c09572d548333f31c8a36922e7c8b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/references/unity-guide.md",
|
||||||
|
"sha256": "76d90bfb9602f49bcd44e4db17933344ffb80f5b49e4d2fb855ee6560852ffe5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/update_version.py",
|
||||||
|
"sha256": "74a387b76612c8f422b5897750e75d7a324eccff7f4e439eec0eafedf0daef43"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/detect_project.py",
|
||||||
|
"sha256": "751907737844a1a7940f8b29c2744ab9517b73e7452f437d67fd0e1d3bbf8d06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/sync_unity_version.py",
|
||||||
|
"sha256": "c69cadbae7246ce82b054213c8aca2a912c2f76e73131ea1caa2e255c0491f13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/changelog_generator.py",
|
||||||
|
"sha256": "a446f1c9d2ec375347468b92a2e8ef2f7eb4da573d64ac389c81bffa256b53d4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/git_operations.py",
|
||||||
|
"sha256": "ee08ce54c2901a40a9e8c4cf543b739cff8138c061ef7ed332fe5a472dc339c1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/assets/CHANGELOG.md.template",
|
||||||
|
"sha256": "9badb0bc8ff7cfc0948778eada3bae7c9fd55b692763dd41564b2a8c4fceb4ea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/assets/version.json.template",
|
||||||
|
"sha256": "a959e111008e5a14623cce9f6e6f79915f27396b9a2096c649e87c678321fe9e"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "73fb86e7061159773e5f4a0f1b0b600e79d7abbf71d4fc7ce8370cce46b3f608"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
294
skills/SKILL.md
Normal file
294
skills/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
---
|
||||||
|
name: auto-release-manager
|
||||||
|
description: Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.). Detect project type, update version files, generate CHANGELOG, and handle git operations with cross-platform support.
|
||||||
|
allowed-tools: Bash, Read, Write
|
||||||
|
---
|
||||||
|
|
||||||
|
# Auto Release Manager
|
||||||
|
|
||||||
|
Automate the entire release workflow for any project type with intelligent project detection and cross-platform support.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This skill streamlines version management across different project types by:
|
||||||
|
- Automatically detecting project type (Node.js, Python, Rust, Unity, Unreal, etc.)
|
||||||
|
- Updating version files in appropriate formats (JSON, TOML, YAML, Unity assets)
|
||||||
|
- Generating CHANGELOG from git commit history
|
||||||
|
- Handling git operations (commit, tag, push) with OS compatibility
|
||||||
|
- Supporting game engine specific workflows (Unity version.json ← → ProjectSettings.asset sync)
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- Releasing a new version of any project
|
||||||
|
- User requests "update version", "create release", "bump version"
|
||||||
|
- Need to handle versions across multiple files (e.g., Unity's dual-file approach)
|
||||||
|
- Want automated CHANGELOG generation from commits
|
||||||
|
- Working with game engine projects (Unity, Unreal)
|
||||||
|
|
||||||
|
Example user requests:
|
||||||
|
- "Bump patch version and create release"
|
||||||
|
- "Update to v2.1.0"
|
||||||
|
- "Create Unity release with version 1.5.0"
|
||||||
|
- "Generate CHANGELOG and commit"
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1: Detect Project Type
|
||||||
|
|
||||||
|
Start by running the project detection script to identify the project type and version files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/detect_project.py .
|
||||||
|
```
|
||||||
|
|
||||||
|
The script returns JSON with:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project_type": "unity",
|
||||||
|
"version_files": [
|
||||||
|
"version.json",
|
||||||
|
"ProjectSettings/ProjectSettings.asset"
|
||||||
|
],
|
||||||
|
"detected_version": "1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported project types:
|
||||||
|
- `nodejs` - Node.js (package.json)
|
||||||
|
- `python` - Python (pyproject.toml, setup.py)
|
||||||
|
- `rust` - Rust (Cargo.toml)
|
||||||
|
- `go` - Go (VERSION file)
|
||||||
|
- `unity` - Unity (version.json + ProjectSettings.asset)
|
||||||
|
- `unreal` - Unreal Engine (.uproject)
|
||||||
|
- `claude-plugin` - Claude Code Plugin (plugin.json)
|
||||||
|
- `generic` - Generic project (VERSION file)
|
||||||
|
|
||||||
|
### Step 2: Determine New Version
|
||||||
|
|
||||||
|
Calculate the new version based on update type:
|
||||||
|
|
||||||
|
**Semantic Versioning (MAJOR.MINOR.PATCH):**
|
||||||
|
- **PATCH** (x.x.X): Bug fixes
|
||||||
|
- **MINOR** (x.X.0): New features (backward compatible)
|
||||||
|
- **MAJOR** (X.0.0): Breaking changes
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- 1.2.3 → 1.2.4 (patch)
|
||||||
|
- 1.2.3 → 1.3.0 (minor)
|
||||||
|
- 1.2.3 → 2.0.0 (major)
|
||||||
|
|
||||||
|
If user specifies version directly (e.g., "v2.1.0"), use that version.
|
||||||
|
|
||||||
|
### Step 3: Update Version Files
|
||||||
|
|
||||||
|
Use the universal version updater to update all detected files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/update_version.py <file1> <file2> ... <new_version>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example for Node.js:
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/update_version.py package.json 1.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Example for Unity (multiple files):
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/update_version.py version.json ProjectSettings/ProjectSettings.asset 1.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unity-specific:** After updating version.json, sync to ProjectSettings.asset:
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/sync_unity_version.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures version.json is the single source of truth.
|
||||||
|
|
||||||
|
### Step 4: Generate CHANGELOG (Optional)
|
||||||
|
|
||||||
|
If user wants CHANGELOG updates or it's part of the workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/changelog_generator.py <new_version> [since_tag] [changelog_path]
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```bash
|
||||||
|
# Generate from last tag to HEAD
|
||||||
|
python -X utf8 scripts/changelog_generator.py 1.3.0 v1.2.3
|
||||||
|
|
||||||
|
# Generate from all commits
|
||||||
|
python -X utf8 scripts/changelog_generator.py 1.3.0
|
||||||
|
|
||||||
|
# Custom CHANGELOG path
|
||||||
|
python -X utf8 scripts/changelog_generator.py 1.3.0 v1.2.3 CHANGELOG.ko.md
|
||||||
|
```
|
||||||
|
|
||||||
|
The script parses Conventional Commits and groups by type:
|
||||||
|
- `feat:` → Added
|
||||||
|
- `fix:` → Fixed
|
||||||
|
- `refactor:` → Changed
|
||||||
|
- `docs:` → Documentation
|
||||||
|
|
||||||
|
### Step 5: Git Operations
|
||||||
|
|
||||||
|
Ask user which git operations to perform using AskUserQuestion tool:
|
||||||
|
|
||||||
|
**Tool constraints:**
|
||||||
|
- Header: "Git 작업" (6 characters ✅)
|
||||||
|
- Options: 3 options (within 2-4 range ✅)
|
||||||
|
- multiSelect: false
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
1. **"커밋만"** (label: 2 words ✅)
|
||||||
|
- Description: "버전 업데이트를 커밋만 합니다. 태그와 푸시는 나중에 직접 처리하겠습니다."
|
||||||
|
- Action: Commit only
|
||||||
|
|
||||||
|
2. **"커밋+태그"** (label: 2 words ✅)
|
||||||
|
- Description: "커밋과 태그를 생성합니다. 푸시는 나중에 직접 하겠습니다."
|
||||||
|
- Action: Commit + Create tag
|
||||||
|
|
||||||
|
3. **"전체"** (label: 1 word ✅)
|
||||||
|
- Description: "커밋, 태그 생성, 푸시까지 모든 작업을 자동으로 처리합니다. (권장)"
|
||||||
|
- Action: Commit + Tag + Push
|
||||||
|
|
||||||
|
Based on user selection:
|
||||||
|
|
||||||
|
**Commit:**
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/git_operations.py commit "chore: bump version to <VERSION>
|
||||||
|
|
||||||
|
Version updates:
|
||||||
|
- Updated <files> to <VERSION>
|
||||||
|
|
||||||
|
🤖 Generated with Claude Code
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Tag:**
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/git_operations.py tag v<VERSION> "Release v<VERSION>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Push (branch + tags):**
|
||||||
|
```bash
|
||||||
|
python -X utf8 scripts/git_operations.py push
|
||||||
|
python -X utf8 scripts/git_operations.py push-tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: GitHub Release (Optional)
|
||||||
|
|
||||||
|
If user wants GitHub release, use `gh` CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh release create v<VERSION> \
|
||||||
|
--title "v<VERSION> - <Title>" \
|
||||||
|
--notes "<Release notes from CHANGELOG>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project-Specific Workflows
|
||||||
|
|
||||||
|
### Unity Workflow
|
||||||
|
|
||||||
|
Unity projects require special handling due to dual-file approach:
|
||||||
|
|
||||||
|
1. Detect Unity project (ProjectSettings.asset exists)
|
||||||
|
2. Update version.json first (source of truth)
|
||||||
|
3. Sync to ProjectSettings.asset using sync_unity_version.py
|
||||||
|
4. Commit both files together
|
||||||
|
5. Tag and push
|
||||||
|
|
||||||
|
Reference: `references/unity-guide.md` for detailed Unity workflow.
|
||||||
|
|
||||||
|
### Unreal Workflow
|
||||||
|
|
||||||
|
Unreal projects use .uproject JSON file:
|
||||||
|
|
||||||
|
1. Detect .uproject file
|
||||||
|
2. Update Version field in .uproject
|
||||||
|
3. Optionally sync to Config/DefaultGame.ini
|
||||||
|
4. Commit, tag, push
|
||||||
|
|
||||||
|
Reference: `references/unreal-guide.md` for detailed Unreal workflow.
|
||||||
|
|
||||||
|
### Node.js / Python / Rust Workflow
|
||||||
|
|
||||||
|
Standard workflow for web/backend projects:
|
||||||
|
|
||||||
|
1. Detect project type via package file
|
||||||
|
2. Update version field in package file
|
||||||
|
3. Generate CHANGELOG from commits
|
||||||
|
4. Commit, tag, push
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Handle common errors:
|
||||||
|
|
||||||
|
**Project not detected:**
|
||||||
|
- Check if running in correct directory
|
||||||
|
- Look for .git directory
|
||||||
|
- Suggest creating VERSION file for generic projects
|
||||||
|
|
||||||
|
**Version file not found:**
|
||||||
|
- Suggest creating file from template (assets/)
|
||||||
|
- For Unity: Create version.json from template
|
||||||
|
|
||||||
|
**Git errors:**
|
||||||
|
- Check if git repository exists
|
||||||
|
- Verify remote is configured
|
||||||
|
- Handle authentication issues
|
||||||
|
|
||||||
|
**Script errors:**
|
||||||
|
- Ensure Python 3.11+ is installed
|
||||||
|
- Check file permissions
|
||||||
|
- Verify file encodings (UTF-8)
|
||||||
|
|
||||||
|
## Bundled Resources
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
All scripts are in `scripts/` directory and work cross-platform (Windows, macOS, Linux):
|
||||||
|
|
||||||
|
- **detect_project.py**: Auto-detect project type
|
||||||
|
- **update_version.py**: Universal version updater
|
||||||
|
- **sync_unity_version.py**: Unity version synchronization
|
||||||
|
- **git_operations.py**: Git workflow automation
|
||||||
|
- **changelog_generator.py**: CHANGELOG generation
|
||||||
|
|
||||||
|
### References
|
||||||
|
|
||||||
|
Detailed guides in `references/` directory:
|
||||||
|
|
||||||
|
- **project-types.md**: All supported project types and version file locations
|
||||||
|
- **unity-guide.md**: Unity-specific version management
|
||||||
|
- **unreal-guide.md**: Unreal Engine version management
|
||||||
|
|
||||||
|
Load these as needed for detailed information.
|
||||||
|
|
||||||
|
### Assets
|
||||||
|
|
||||||
|
Templates in `assets/` directory:
|
||||||
|
|
||||||
|
- **version.json.template**: Template for Unity/game projects
|
||||||
|
- **CHANGELOG.md.template**: Template for new CHANGELOG files
|
||||||
|
|
||||||
|
Use these when creating new version files.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Always commit version files together (e.g., Unity's version.json + ProjectSettings.asset)
|
||||||
|
- Use semantic versioning consistently
|
||||||
|
- Write meaningful commit messages following Conventional Commits
|
||||||
|
- Tag releases with `v` prefix (e.g., v1.2.3)
|
||||||
|
- Keep CHANGELOG updated for user-facing changes
|
||||||
|
- For Unity: version.json is source of truth, always sync to ProjectSettings.asset
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Scripts use Python 3.11+ with only stdlib dependencies
|
||||||
|
- All file operations use UTF-8 encoding
|
||||||
|
- Path handling uses pathlib for cross-platform compatibility
|
||||||
|
- Git operations use subprocess for reliability
|
||||||
|
- Unity YAML parsing uses regex for robustness
|
||||||
16
skills/assets/CHANGELOG.md.template
Normal file
16
skills/assets/CHANGELOG.md.template
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial project setup
|
||||||
|
|
||||||
|
## [0.1.0] - 2025-11-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- First release
|
||||||
6
skills/assets/version.json.template
Normal file
6
skills/assets/version.json.template
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"buildNumber": 1,
|
||||||
|
"releaseDate": "2025-11-01",
|
||||||
|
"releaseNotes": "Initial release"
|
||||||
|
}
|
||||||
160
skills/references/project-types.md
Normal file
160
skills/references/project-types.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Supported Project Types
|
||||||
|
|
||||||
|
This document describes all project types supported by auto-release-manager and their version file locations.
|
||||||
|
|
||||||
|
## Web & Backend
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
- **Detection file**: `package.json`
|
||||||
|
- **Version location**: `package.json` → `version` field
|
||||||
|
- **Format**: JSON
|
||||||
|
- **Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-project",
|
||||||
|
"version": "1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
- **Detection files**: `pyproject.toml` or `setup.py`
|
||||||
|
- **Version location**:
|
||||||
|
- `pyproject.toml` → `project.version` or `tool.poetry.version`
|
||||||
|
- `setup.py` → `version=` parameter
|
||||||
|
- **Format**: TOML or Python
|
||||||
|
- **Example**:
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "my-project"
|
||||||
|
version = "1.2.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust
|
||||||
|
- **Detection file**: `Cargo.toml`
|
||||||
|
- **Version location**: `Cargo.toml` → `package.version`
|
||||||
|
- **Format**: TOML
|
||||||
|
- **Example**:
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "my-project"
|
||||||
|
version = "1.2.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
- **Detection file**: `go.mod`
|
||||||
|
- **Version location**: `VERSION` file (custom)
|
||||||
|
- **Format**: Plain text
|
||||||
|
- **Note**: Go doesn't have built-in versioning in go.mod
|
||||||
|
|
||||||
|
## Game Engines
|
||||||
|
|
||||||
|
### Unity
|
||||||
|
- **Detection file**: `ProjectSettings/ProjectSettings.asset`
|
||||||
|
- **Version locations**:
|
||||||
|
1. `version.json` (Primary source of truth) → `version` field
|
||||||
|
2. `ProjectSettings/ProjectSettings.asset` → `bundleVersion` field
|
||||||
|
- **Format**: JSON + Unity YAML
|
||||||
|
- **Workflow**: Update `version.json` first, then sync to `ProjectSettings.asset`
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
`version.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.2.3",
|
||||||
|
"buildNumber": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProjectSettings.asset`:
|
||||||
|
```yaml
|
||||||
|
PlayerSettings:
|
||||||
|
bundleVersion: 1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unreal Engine
|
||||||
|
- **Detection file**: `*.uproject`
|
||||||
|
- **Version location**: `<ProjectName>.uproject` → `Version` or `EngineAssociation`
|
||||||
|
- **Format**: JSON
|
||||||
|
- **Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"FileVersion": 3,
|
||||||
|
"EngineAssociation": "5.3",
|
||||||
|
"Version": "1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Godot
|
||||||
|
- **Detection file**: `project.godot`
|
||||||
|
- **Version location**: `project.godot` → `config/version`
|
||||||
|
- **Format**: INI-like
|
||||||
|
- **Example**:
|
||||||
|
```ini
|
||||||
|
[application]
|
||||||
|
config/name="MyGame"
|
||||||
|
config/version="1.2.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugins & Extensions
|
||||||
|
|
||||||
|
### Claude Code Plugin
|
||||||
|
- **Detection file**: `.claude-plugin/plugin.json`
|
||||||
|
- **Version location**: `.claude-plugin/plugin.json` → `version`
|
||||||
|
- **Format**: JSON
|
||||||
|
- **Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-plugin",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"description": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code Extension
|
||||||
|
- **Detection file**: `package.json` + `package.nls.json`
|
||||||
|
- **Version location**: `package.json` → `version`
|
||||||
|
- **Format**: JSON
|
||||||
|
- **Same as Node.js projects**
|
||||||
|
|
||||||
|
### Browser Extension
|
||||||
|
- **Detection file**: `manifest.json`
|
||||||
|
- **Version location**: `manifest.json` → `version`
|
||||||
|
- **Format**: JSON
|
||||||
|
- **Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "My Extension",
|
||||||
|
"version": "1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic Projects
|
||||||
|
|
||||||
|
### Plain VERSION file
|
||||||
|
- **Detection file**: `VERSION` or `version.txt`
|
||||||
|
- **Version location**: Content of file
|
||||||
|
- **Format**: Plain text
|
||||||
|
- **Example**:
|
||||||
|
```
|
||||||
|
1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Format
|
||||||
|
|
||||||
|
All projects follow **Semantic Versioning (semver)**:
|
||||||
|
|
||||||
|
```
|
||||||
|
MAJOR.MINOR.PATCH
|
||||||
|
|
||||||
|
1.2.3
|
||||||
|
│ │ │
|
||||||
|
│ │ └─ PATCH: Bug fixes
|
||||||
|
│ └─── MINOR: New features (backward compatible)
|
||||||
|
└───── MAJOR: Breaking changes
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `1.0.0` → `1.0.1` (patch: bug fix)
|
||||||
|
- `1.0.1` → `1.1.0` (minor: new feature)
|
||||||
|
- `1.1.0` → `2.0.0` (major: breaking change)
|
||||||
140
skills/references/unity-guide.md
Normal file
140
skills/references/unity-guide.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Unity Project Version Management
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Unity projects use a dual-file approach for version management:
|
||||||
|
1. **`version.json`** - Single source of truth (easy to read/edit)
|
||||||
|
2. **`ProjectSettings/ProjectSettings.asset`** - Auto-synced (used by Unity builds)
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
MyUnityProject/
|
||||||
|
├── version.json # Primary source
|
||||||
|
├── ProjectSettings/
|
||||||
|
│ └── ProjectSettings.asset # Auto-synced
|
||||||
|
└── Assets/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## version.json (Primary)
|
||||||
|
|
||||||
|
Create this file at project root:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.2.3",
|
||||||
|
"buildNumber": 42,
|
||||||
|
"releaseDate": "2025-10-20"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `version` (required): Semantic version (MAJOR.MINOR.PATCH)
|
||||||
|
- `buildNumber` (optional): Incremental build number
|
||||||
|
- `releaseDate` (optional): Release date in YYYY-MM-DD format
|
||||||
|
|
||||||
|
## ProjectSettings.asset (Auto-sync)
|
||||||
|
|
||||||
|
The `bundleVersion` field in this file is automatically synced from `version.json`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
PlayerSettings:
|
||||||
|
bundleVersion: 1.2.3
|
||||||
|
AndroidBundleVersionCode: 42
|
||||||
|
buildNumber:
|
||||||
|
iOS: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Manual Update
|
||||||
|
|
||||||
|
1. Edit `version.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.3.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run sync script:
|
||||||
|
```bash
|
||||||
|
python scripts/sync_unity_version.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Commit both files:
|
||||||
|
```bash
|
||||||
|
git add version.json ProjectSettings/ProjectSettings.asset
|
||||||
|
git commit -m "chore: bump version to 1.3.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Update (with auto-release-manager)
|
||||||
|
|
||||||
|
The skill handles everything automatically:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Update version.json → 1.3.0
|
||||||
|
2. Sync to ProjectSettings.asset
|
||||||
|
3. Git commit both files
|
||||||
|
4. Create tag v1.3.0
|
||||||
|
5. Push to remote
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of Dual-File Approach
|
||||||
|
|
||||||
|
✅ **Easy Version Management**
|
||||||
|
- `version.json` is simple JSON, easy to read/edit
|
||||||
|
- Clean git diffs
|
||||||
|
|
||||||
|
✅ **Unity Integration**
|
||||||
|
- `ProjectSettings.asset` is used by Unity builds
|
||||||
|
- Version automatically appears in build
|
||||||
|
|
||||||
|
✅ **Automated Sync**
|
||||||
|
- Scripts ensure consistency
|
||||||
|
- No manual errors
|
||||||
|
|
||||||
|
✅ **Extended Metadata**
|
||||||
|
- `version.json` can store additional info (buildNumber, releaseDate)
|
||||||
|
- `ProjectSettings.asset` only has bundleVersion
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Q: Why not just use ProjectSettings.asset?
|
||||||
|
|
||||||
|
A: ProjectSettings.asset is a YAML file that's hard to read and edit manually. It also contains hundreds of other settings, making git diffs noisy.
|
||||||
|
|
||||||
|
### Q: Can I use only version.json?
|
||||||
|
|
||||||
|
A: No. Unity reads bundleVersion from ProjectSettings.asset for builds. Both files are needed.
|
||||||
|
|
||||||
|
### Q: What if files get out of sync?
|
||||||
|
|
||||||
|
A: Run `python scripts/sync_unity_version.py` to sync from version.json (source of truth) to ProjectSettings.asset.
|
||||||
|
|
||||||
|
## Example: Version Update
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```json
|
||||||
|
// version.json
|
||||||
|
{
|
||||||
|
"version": "1.2.3",
|
||||||
|
"buildNumber": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```json
|
||||||
|
// version.json
|
||||||
|
{
|
||||||
|
"version": "1.3.0",
|
||||||
|
"buildNumber": 43
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auto-synced:**
|
||||||
|
```yaml
|
||||||
|
# ProjectSettings.asset
|
||||||
|
PlayerSettings:
|
||||||
|
bundleVersion: 1.3.0 # ← Automatically updated
|
||||||
|
```
|
||||||
152
skills/references/unreal-guide.md
Normal file
152
skills/references/unreal-guide.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# Unreal Engine Project Version Management
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Unreal Engine projects use `.uproject` JSON file for project configuration and versioning.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
MyUnrealProject/
|
||||||
|
├── MyGame.uproject # Project file with version
|
||||||
|
├── Config/
|
||||||
|
│ └── DefaultGame.ini # Game config
|
||||||
|
├── Source/
|
||||||
|
│ └── ...
|
||||||
|
└── Content/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## .uproject File
|
||||||
|
|
||||||
|
The project file is a JSON file containing project metadata:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"FileVersion": 3,
|
||||||
|
"EngineAssociation": "5.3",
|
||||||
|
"Category": "",
|
||||||
|
"Description": "",
|
||||||
|
"Version": "1.2.3",
|
||||||
|
"Modules": [
|
||||||
|
{
|
||||||
|
"Name": "MyGame",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Plugins": [
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Version Fields:**
|
||||||
|
- `Version` (custom): Semantic version of your project
|
||||||
|
- `EngineAssociation`: Unreal Engine version (e.g., "5.3", "5.4")
|
||||||
|
|
||||||
|
## Version Update Workflow
|
||||||
|
|
||||||
|
### Manual Update
|
||||||
|
|
||||||
|
1. Edit `.uproject` file:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "1.3.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Commit:
|
||||||
|
```bash
|
||||||
|
git add MyGame.uproject
|
||||||
|
git commit -m "chore: bump version to 1.3.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Update (with auto-release-manager)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Detect .uproject file
|
||||||
|
2. Update Version field → 1.3.0
|
||||||
|
3. Git commit
|
||||||
|
4. Create tag v1.3.0
|
||||||
|
5. Push to remote
|
||||||
|
```
|
||||||
|
|
||||||
|
## DefaultGame.ini
|
||||||
|
|
||||||
|
Optional: You can also store version in `Config/DefaultGame.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/EngineSettings.GeneralProjectSettings]
|
||||||
|
ProjectID=...
|
||||||
|
ProjectName=MyGame
|
||||||
|
ProjectVersion=1.2.3
|
||||||
|
CompanyName=MyCompany
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for in-game version display.
|
||||||
|
|
||||||
|
## Build Versioning
|
||||||
|
|
||||||
|
For packaging builds, you may want to configure:
|
||||||
|
|
||||||
|
**Config/DefaultGame.ini:**
|
||||||
|
```ini
|
||||||
|
[/Script/UnrealEd.ProjectPackagingSettings]
|
||||||
|
BuildConfiguration=PPBC_Shipping
|
||||||
|
ProjectVersion=1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Full Update
|
||||||
|
|
||||||
|
When updating version from 1.2.3 to 1.3.0:
|
||||||
|
|
||||||
|
**Files to update:**
|
||||||
|
1. `MyGame.uproject` → `Version: "1.3.0"`
|
||||||
|
2. `Config/DefaultGame.ini` → `ProjectVersion=1.3.0` (if used)
|
||||||
|
|
||||||
|
**auto-release-manager handles:**
|
||||||
|
- Detecting .uproject file
|
||||||
|
- Updating Version field
|
||||||
|
- Git commit + tag
|
||||||
|
- Push to remote
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Q: Is Version field required in .uproject?
|
||||||
|
|
||||||
|
A: No, it's optional. But recommended for version tracking.
|
||||||
|
|
||||||
|
### Q: What about EngineAssociation?
|
||||||
|
|
||||||
|
A: EngineAssociation is the UE version (e.g., "5.3"). Don't confuse it with your project version.
|
||||||
|
|
||||||
|
### Q: Can I use multiple version fields?
|
||||||
|
|
||||||
|
A: Yes! You can maintain:
|
||||||
|
- `.uproject` → Version (project version)
|
||||||
|
- `DefaultGame.ini` → ProjectVersion (in-game display)
|
||||||
|
- `version.json` → Custom metadata
|
||||||
|
|
||||||
|
Just ensure they stay synced.
|
||||||
|
|
||||||
|
## Recommended Approach
|
||||||
|
|
||||||
|
**Best Practice:**
|
||||||
|
1. Use `.uproject` → Version as primary
|
||||||
|
2. Optionally sync to `DefaultGame.ini` for in-game display
|
||||||
|
3. Use auto-release-manager to keep everything consistent
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```json
|
||||||
|
// MyGame.uproject
|
||||||
|
{
|
||||||
|
"Version": "1.3.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
; Config/DefaultGame.ini
|
||||||
|
[/Script/EngineSettings.GeneralProjectSettings]
|
||||||
|
ProjectVersion=1.3.0
|
||||||
|
```
|
||||||
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