mirror of
https://github.com/jpush/jpush-phonegap-plugin.git
synced 2026-05-27 00:00:12 +08:00
更新skill
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: update-sdk
|
||||
description: |
|
||||
更新 jpush-phonegap-plugin 插件的 JPush SDK 版本。自动拉取极光官网 Changelog,分析新增/移除/变更 API,更新 plugin.xml 中 Android(Gradle framework)和 iOS(CocoaPods podspec)版本引用,同步更新 Native 层(Java/ObjC)和 JS Bridge 层代码,展示变更摘要确认后发布到 npm。
|
||||
Use when: 更新 JPush SDK、升级推送 SDK 版本、jpush-phonegap-plugin 发布新版本、Cordova/PhoneGap 插件 SDK 更新。
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Edit
|
||||
- Write
|
||||
- WebFetch
|
||||
---
|
||||
|
||||
你正在更新 **jpush-phonegap-plugin** 插件。
|
||||
|
||||
**用户参数:** $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## 第一步:解析参数
|
||||
|
||||
从 `$ARGUMENTS` 中提取版本号:
|
||||
- `--android X.X.X` → Android JPush SDK 目标版本
|
||||
- `--ios X.X.X` → iOS JPush SDK 目标版本
|
||||
|
||||
如果某端版本号缺失,先询问用户再继续。
|
||||
|
||||
---
|
||||
|
||||
## 第二步:安装依赖
|
||||
|
||||
```bash
|
||||
pip3 install requests beautifulsoup4 -q 2>&1 | tail -1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第三步:拉取 SDK Changelog
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/update-sdk/scripts/changelog_fetcher.py --android <ANDROID_VERSION> --ios <IOS_VERSION>
|
||||
```
|
||||
|
||||
读取 `.claude/skills/update-sdk/scripts/.changelog_cache.json` 获取 Changelog 内容。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:AI 分析变更
|
||||
|
||||
分析 Changelog,整理:
|
||||
|
||||
> **注意**:Changelog 同时包含 JPush 和 JCore 的变更。只关注调用类以 `JPush` 开头的条目,忽略类名以 `JCore` 开头的内容。
|
||||
|
||||
1. 新增 API(Android + iOS 相同功能合并为统一插件 API);仅单端有的 → **先检查另一端是否已有等价实现**(见下方说明),确认缺失才标注单端
|
||||
2. 移除/废弃 API
|
||||
3. 行为变更
|
||||
4. 新插件版本号:始终升 patch(如 3.4.9 → 3.5.0,3.9.9 → 4.0.0)
|
||||
|
||||
> **跨平台等价检查**:当 Changelog 只在某一端出现新增 API 时,**不要直接标为单端 Only**。先读取另一端的 Native 文件(`src/android/JPushPlugin.java` 或 `src/ios/Plugins/JPushPlugin.m`)和 JS Bridge(`www/JPushPlugin.js`),搜索功能相同或名称相近的方法。如果另一端已有对应实现,则合并为统一 API;只有确认另一端完全没有等价功能时,才标注 Android Only / iOS Only。
|
||||
|
||||
---
|
||||
|
||||
## 第五步:更新版本号引用
|
||||
|
||||
plugin.xml 中同时包含 SDK 版本和插件版本:
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/update-sdk/scripts/plugin_updater.py \
|
||||
--android <ANDROID_VERSION> \
|
||||
--ios <IOS_VERSION> \
|
||||
--bump-patch \
|
||||
--changelog-summary "<ONE_LINE_SUMMARY>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第六步:更新 Native 层代码
|
||||
|
||||
**编写代码前,先通过 WebFetch 查询官网 API 文档,确认新增方法的完整签名、参数类型和返回值:**
|
||||
- Android 文档:`https://docs.jiguang.cn/jpush/client/Android/android_api`
|
||||
- iOS 文档:`https://docs.jiguang.cn/jpush/client/iOS/ios_api`
|
||||
|
||||
在文档中搜索第四步识别出的新增方法名,确认签名后再编写下方代码。
|
||||
|
||||
**Android** — `src/android/JPushPlugin.java`
|
||||
- 在 `execute()` 方法中添加新的 action 分支处理新 API
|
||||
- 内部调用 `JPushInterface.newMethod()`
|
||||
|
||||
**iOS** — `src/ios/Plugins/JPushPlugin.m`
|
||||
- 在对应的 action 处理中添加新方法
|
||||
- 内部调用 JPush iOS SDK 对应方法
|
||||
|
||||
---
|
||||
|
||||
## 第七步:更新 JavaScript Bridge
|
||||
|
||||
**`www/JPushPlugin.js`**
|
||||
- 添加新方法的 `cordova.exec()` 封装(统一 Android 和 iOS 入口)
|
||||
|
||||
---
|
||||
|
||||
## 第八步:展示变更摘要并请求确认
|
||||
|
||||
```
|
||||
========== jpush-phonegap-plugin 更新摘要 ==========
|
||||
Android JPush SDK: 旧版本 → 新版本
|
||||
iOS JPush SDK: 旧版本 → 新版本
|
||||
插件版本(plugin.xml):旧版本 → 新版本
|
||||
|
||||
新增 API:...
|
||||
修改的文件:plugin.xml, src/android/JPushPlugin.java, src/ios/Plugins/JPushPlugin.m, www/JPushPlugin.js, CHANGELOG.md
|
||||
====================================================
|
||||
|
||||
确认以上变更并发布到 npm? [y/N]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第九步:发布(确认后执行)
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/update-sdk/scripts/publisher.py
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"android": "## JPush Android SDK v6.1.0\n### 更新时间\n\n+ 2026-04-27\n\n### Change Log\n\n+ 新增接口[setKeepLongConnInBackground](https://docs.jiguang.cn/jpush/client/Android/android_api#%E9%80%80%E5%90%8E%E5%8F%B0%E6%98%AF%E5%90%A6%E4%BF%9D%E6%8C%81%E6%9E%81%E5%85%89%E9%95%BF%E8%BF%9E%E6%8E%A5-api)\n + 应用退至后台是否保持极光长连接的配置接口\n+ 新增支持厂商 VOIP 能力支持\n + [JPushMessageReceiver 新增 onVoipMessage 回调](https://docs.jiguang.cn/jpush/client/Android/android_api#jpushmessagereceiver-%E5%9B%9E%E8%B0%83%E7%B1%BB)\n+ 更新厂商推送 SDK,各厂商推送 SDK 版本如下\n + 华为推送 SDK V6.13.0.301\n + 荣耀推送 SDK V10.0.13.305\n + 小米推送 SDK V7.9.2\n + OPPO 推送 SDK V3.9.8\n + VIVO 推送 SDK V4.1.3.0\n + 特别注意,JPush Android SDK v5.2.0及其以上版本,请关注 vivo 通道集成指南 更新。\n + 魅族推送 SDK V5.0.5\n + 特别注意,JPush Android SDK v5.2.2及其以上版本,请关注 魅族 通道集成指南 更新。\n + FCM 推送 SDK V24.1.2\n + 蔚来 SDK 从此次版本开始不再支持,具体参考[蔚来通道 Push 自动切换至极光通道通知](https://community.jiguang.cn/notice/232)\n+ 修复已知问题\n\n### 升级提示\n\n+ 必须配合 JCore Android SDK v5.4.0 及以上版本使用\n+ JPush Android SDK v5.9.0 及其以上版本,各厂商 mavenCentral 集成方式优化\n + 依赖包会自动集成,无需再手动添加jar和aar,具体参考极光[厂商通道集成指南](https://docs.jiguang.cn/jpush/client/Android/android_3rd_guide)\n+ JPush Android SDK v5.2.0 及其以上版本升级到 JPush Android SDK v5.4.0 及其以上版本时,需要将消息回调方式从 JPushMessageService 改为 JPushMessageReceiver\n + 详情请查看极光文档:[Android SDK 集成指南](https://docs.jiguang.cn/jpush/client/Android/android_guide)",
|
||||
"ios": "## JPush iOS SDK v6.1.0\n### 更新时间\n+ 2026-04-27\n\n### Change Log\n+ 修复已知问题\n> 注意:JPush iOS SDK 4.9.0 及以上版本需要使用 Xcode 14 及以上版本,请注意升级!\n\n### 升级提示\n+ 建议配合 JCore iOS SDK v5.4.0 及以上版本使用\n+ 升级到 JPush iOS SDK 4.9.0 及以上版本需删除 armv7 和 armv7s 架构"
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fetch SDK changelog from JiGuang official docs for a specific version."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("ERROR: Missing dependencies. Run: pip3 install requests")
|
||||
sys.exit(1)
|
||||
|
||||
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
|
||||
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
|
||||
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def fetch_changelog(url: str, target_version: str) -> str:
|
||||
"""Fetch and extract changelog section for target_version from a Markdown URL."""
|
||||
md_url = url if url.endswith(".md") else url + ".md"
|
||||
|
||||
try:
|
||||
resp = requests.get(md_url, headers=HEADERS, timeout=30)
|
||||
resp.raise_for_status()
|
||||
content = resp.text
|
||||
except requests.RequestException as e:
|
||||
return f"ERROR: Failed to fetch {md_url}: {e}"
|
||||
|
||||
lines = content.split("\n")
|
||||
in_section = False
|
||||
section_lines = []
|
||||
|
||||
for line in lines:
|
||||
# Match headings like: ## JPush Android SDK v6.1.0
|
||||
if re.match(r'^##\s+.*v?' + re.escape(target_version) + r'\b', line):
|
||||
in_section = True
|
||||
section_lines.append(line)
|
||||
elif in_section:
|
||||
if re.match(r'^##\s+', line):
|
||||
break # next version section starts
|
||||
section_lines.append(line)
|
||||
|
||||
if not section_lines:
|
||||
return f"VERSION_NOT_FOUND: Could not find changelog for version {target_version} at {md_url}"
|
||||
|
||||
return "\n".join(section_lines).strip()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Fetch JiGuang SDK changelog")
|
||||
parser.add_argument("--android", help="Android SDK version")
|
||||
parser.add_argument("--ios", help="iOS SDK version")
|
||||
parser.add_argument("--harmony", help="HarmonyOS SDK version")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config()
|
||||
urls = config.get("changelog_urls", {})
|
||||
result = {}
|
||||
|
||||
platforms = [
|
||||
("android", args.android),
|
||||
("ios", args.ios),
|
||||
("harmony", args.harmony),
|
||||
]
|
||||
|
||||
for platform, version in platforms:
|
||||
if version and platform in urls:
|
||||
print(f"\n=== Fetching {platform} changelog for v{version} ===")
|
||||
content = fetch_changelog(urls[platform], version)
|
||||
result[platform] = content
|
||||
print(content)
|
||||
|
||||
cache_path = ".claude/skills/update-sdk/scripts/.changelog_cache.json"
|
||||
with open(cache_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n=== Changelog saved to {cache_path} ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"plugin_name": "jpush-phonegap-plugin",
|
||||
"sdk_type": "jpush",
|
||||
"platform": "cordova",
|
||||
"changelog_urls": {
|
||||
"android": "https://docs.jiguang.cn/jpush/jpush_changelog/updates_Android",
|
||||
"ios": "https://docs.jiguang.cn/jpush/jpush_changelog/updates_iOS"
|
||||
},
|
||||
"version_refs": {
|
||||
"android_sdk": {
|
||||
"integration": "cordova-framework",
|
||||
"files": [
|
||||
{
|
||||
"path": "plugin.xml",
|
||||
"pattern": "<framework src=\"cn.jiguang.sdk:jpush:{VERSION}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"ios_sdk": {
|
||||
"integration": "cocoapods-podspec",
|
||||
"files": [
|
||||
{
|
||||
"path": "plugin.xml",
|
||||
"pattern": "<pod name=\"JPush\" spec=\"{VERSION}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugin": {
|
||||
"files": [
|
||||
{
|
||||
"path": "package.json",
|
||||
"key": "version"
|
||||
},
|
||||
{
|
||||
"path": "plugin.xml",
|
||||
"key": "version",
|
||||
"pattern": "id=\"jpush-phonegap-plugin\"\n version=\"{VERSION}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"native_files": {
|
||||
"android": [
|
||||
"src/android/JPushPlugin.java"
|
||||
],
|
||||
"ios": [
|
||||
"src/ios/Plugins/JPushPlugin.h",
|
||||
"src/ios/Plugins/JPushPlugin.m"
|
||||
]
|
||||
},
|
||||
"bridge_files": {
|
||||
"js": [
|
||||
"www/JPushPlugin.js"
|
||||
]
|
||||
},
|
||||
"changelog_file": "CHANGELOG.md",
|
||||
"publish": {
|
||||
"type": "npm"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Update SDK version references in plugin files based on config.json."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
|
||||
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def bump_patch(version: str) -> str:
|
||||
"""Bump patch version. patch and minor cap at 9; carry over to next component at 10."""
|
||||
parts = version.split(".")
|
||||
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
||||
patch += 1
|
||||
if patch >= 10:
|
||||
patch = 0
|
||||
minor += 1
|
||||
if minor >= 10:
|
||||
minor = 0
|
||||
major += 1
|
||||
return f"{major}.{minor}.{patch}"
|
||||
|
||||
|
||||
def get_current_version_from_ref(ref: dict) -> str | None:
|
||||
"""Read the current version string from a file ref."""
|
||||
path = Path(ref["path"])
|
||||
if not path.exists():
|
||||
return None
|
||||
content = path.read_text(encoding="utf-8")
|
||||
|
||||
if "pattern" in ref:
|
||||
regex = re.escape(ref["pattern"]).replace(r"\{VERSION\}", r"([\d.]+(?:-\w+)?)")
|
||||
match = re.search(regex, content)
|
||||
return match.group(1) if match else None
|
||||
elif "key" in ref:
|
||||
if path.suffix in (".yaml", ".yml"):
|
||||
match = re.search(r"^version:\s*(.+)$", content, re.MULTILINE)
|
||||
return match.group(1).strip() if match else None
|
||||
elif path.suffix == ".xml":
|
||||
key = ref["key"]
|
||||
match = re.search(rf'{re.escape(key)}="([^"]+)"', content)
|
||||
return match.group(1) if match else None
|
||||
else: # JSON
|
||||
try:
|
||||
data = json.loads(content)
|
||||
keys = ref["key"].split(".")
|
||||
obj = data
|
||||
for k in keys:
|
||||
obj = obj[k]
|
||||
return str(obj)
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def update_by_pattern(file_path: str, pattern: str, new_version: str) -> bool:
|
||||
"""Replace version in file using a pattern with {VERSION} placeholder."""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
print(f" WARNING: File not found: {file_path}")
|
||||
return False
|
||||
|
||||
content = path.read_text(encoding="utf-8")
|
||||
regex = re.escape(pattern).replace(r"\{VERSION\}", r"([\d.]+(?:-\w+)?)")
|
||||
replacement = pattern.replace("{VERSION}", new_version)
|
||||
|
||||
match = re.search(regex, content)
|
||||
if not match:
|
||||
print(f" WARNING: Pattern not found in {file_path}")
|
||||
print(f" Pattern: {pattern}")
|
||||
return False
|
||||
|
||||
old_version = match.group(1)
|
||||
if old_version == new_version:
|
||||
print(f" SKIP: {file_path} already at {new_version}")
|
||||
return True
|
||||
|
||||
new_content = content.replace(match.group(0), replacement, 1)
|
||||
path.write_text(new_content, encoding="utf-8")
|
||||
print(f" UPDATED: {file_path} {old_version} → {new_version}")
|
||||
return True
|
||||
|
||||
|
||||
def update_json_field(file_path: str, key: str, new_version: str) -> bool:
|
||||
"""Update a field in a JSON file (supports dot-notation keys)."""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
print(f" WARNING: File not found: {file_path}")
|
||||
return False
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
keys = key.split(".")
|
||||
obj = data
|
||||
for k in keys[:-1]:
|
||||
obj = obj.setdefault(k, {})
|
||||
|
||||
old = obj.get(keys[-1], "unknown")
|
||||
if old == new_version:
|
||||
print(f" SKIP: {file_path} [{key}] already at {new_version}")
|
||||
return True
|
||||
|
||||
obj[keys[-1]] = new_version
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
|
||||
print(f" UPDATED: {file_path} [{key}] {old} → {new_version}")
|
||||
return True
|
||||
|
||||
|
||||
def update_yaml_version(file_path: str, new_version: str) -> bool:
|
||||
"""Update 'version:' line in a YAML file."""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
print(f" WARNING: File not found: {file_path}")
|
||||
return False
|
||||
|
||||
content = path.read_text(encoding="utf-8")
|
||||
match = re.search(r"^(version:\s*)(.+)$", content, re.MULTILINE)
|
||||
if not match:
|
||||
print(f" WARNING: No 'version:' field found in {file_path}")
|
||||
return False
|
||||
|
||||
old_version = match.group(2).strip()
|
||||
if old_version == new_version:
|
||||
print(f" SKIP: {file_path} already at {new_version}")
|
||||
return True
|
||||
|
||||
new_content = content.replace(match.group(0), f"{match.group(1)}{new_version}", 1)
|
||||
path.write_text(new_content, encoding="utf-8")
|
||||
print(f" UPDATED: {file_path} {old_version} → {new_version}")
|
||||
return True
|
||||
|
||||
|
||||
def update_xml_attribute(file_path: str, attribute: str, new_version: str) -> bool:
|
||||
"""Update a version attribute in an XML file (e.g., plugin.xml version="X.X.X")."""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
print(f" WARNING: File not found: {file_path}")
|
||||
return False
|
||||
|
||||
content = path.read_text(encoding="utf-8")
|
||||
pattern = rf'({re.escape(attribute)}=")([^"]+)(")'
|
||||
match = re.search(pattern, content)
|
||||
if not match:
|
||||
print(f" WARNING: Attribute '{attribute}' not found in {file_path}")
|
||||
return False
|
||||
|
||||
old_version = match.group(2)
|
||||
if old_version == new_version:
|
||||
print(f" SKIP: {file_path} [{attribute}] already at {new_version}")
|
||||
return True
|
||||
|
||||
new_content = content.replace(match.group(0), f'{match.group(1)}{new_version}{match.group(3)}', 1)
|
||||
path.write_text(new_content, encoding="utf-8")
|
||||
print(f" UPDATED: {file_path} [{attribute}] {old_version} → {new_version}")
|
||||
return True
|
||||
|
||||
|
||||
def update_config_json_dep(file_path: str, dep_prefix: str, new_version: str) -> bool:
|
||||
"""Update a maven dependency version in a UTS config.json dependencies array."""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
print(f" WARNING: File not found: {file_path}")
|
||||
return False
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
deps = data.get("dependencies", [])
|
||||
updated = False
|
||||
for i, dep in enumerate(deps):
|
||||
if isinstance(dep, str) and dep.startswith(dep_prefix):
|
||||
old = dep
|
||||
parts = dep.rsplit(":", 1)
|
||||
deps[i] = f"{parts[0]}:{new_version}"
|
||||
print(f" UPDATED: {file_path} dep {old} → {deps[i]}")
|
||||
updated = True
|
||||
|
||||
if not updated:
|
||||
print(f" WARNING: Dependency prefix '{dep_prefix}' not found in {file_path}")
|
||||
return False
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent="\t", ensure_ascii=False)
|
||||
f.write("\n")
|
||||
return True
|
||||
|
||||
|
||||
def update_changelog(changelog_file: str, plugin_version: str, summary: str):
|
||||
path = Path(changelog_file)
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
entry = f"## {plugin_version} ({date_str})\n\n{summary}\n\n"
|
||||
existing = path.read_text(encoding="utf-8") if path.exists() else "# Changelog\n\n"
|
||||
path.write_text(entry + existing, encoding="utf-8")
|
||||
print(f" UPDATED: {changelog_file} → added v{plugin_version} entry")
|
||||
|
||||
|
||||
def process_file_ref(ref: dict, version: str) -> bool:
|
||||
path = ref["path"]
|
||||
if "pattern" in ref:
|
||||
return update_by_pattern(path, ref["pattern"], version)
|
||||
elif "key" in ref:
|
||||
if path.endswith(".yaml") or path.endswith(".yml"):
|
||||
return update_yaml_version(path, version)
|
||||
elif path.endswith(".xml"):
|
||||
return update_xml_attribute(path, ref["key"], version)
|
||||
else:
|
||||
return update_json_field(path, ref["key"], version)
|
||||
elif "dep_prefix" in ref:
|
||||
return update_config_json_dep(path, ref["dep_prefix"], version)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Update SDK version references")
|
||||
parser.add_argument("--android", help="Android SDK version")
|
||||
parser.add_argument("--ios", help="iOS SDK version")
|
||||
parser.add_argument("--plugin-version", help="Explicit new plugin version")
|
||||
parser.add_argument("--bump-patch", action="store_true",
|
||||
help="Auto-bump patch (carry at 10: x.y.9→x.(y+1).0, x.9.9→(x+1).0.0)")
|
||||
parser.add_argument("--changelog-summary", default="", help="Summary for CHANGELOG.md")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config()
|
||||
refs = config.get("version_refs", {})
|
||||
|
||||
print("\n=== Updating version references ===")
|
||||
|
||||
if args.android and "android_sdk" in refs:
|
||||
print(f"\n[Android SDK → {args.android}]")
|
||||
for ref in refs["android_sdk"].get("files", []):
|
||||
process_file_ref(ref, args.android)
|
||||
|
||||
if args.ios and "ios_sdk" in refs:
|
||||
print(f"\n[iOS SDK → {args.ios}]")
|
||||
for ref in refs["ios_sdk"].get("files", []):
|
||||
process_file_ref(ref, args.ios)
|
||||
|
||||
# Resolve plugin version
|
||||
plugin_version = args.plugin_version
|
||||
if args.bump_patch and not plugin_version and "plugin" in refs:
|
||||
for ref in refs["plugin"].get("files", []):
|
||||
current = get_current_version_from_ref(ref)
|
||||
if current:
|
||||
plugin_version = bump_patch(current)
|
||||
print(f"\n[Auto bump-patch: {current} → {plugin_version}]")
|
||||
break
|
||||
if not plugin_version:
|
||||
print(" ERROR: Could not read current plugin version for bump-patch")
|
||||
sys.exit(1)
|
||||
|
||||
if plugin_version and "plugin" in refs:
|
||||
print(f"\n[Plugin version → {plugin_version}]")
|
||||
for ref in refs["plugin"].get("files", []):
|
||||
process_file_ref(ref, plugin_version)
|
||||
|
||||
if plugin_version and args.changelog_summary:
|
||||
changelog_file = config.get("changelog_file", "CHANGELOG.md")
|
||||
update_changelog(changelog_file, plugin_version, args.changelog_summary)
|
||||
|
||||
print("\n=== Version update complete ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish plugin to package registry and create git tag."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
|
||||
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def run(cmd: list, cwd: str = ".") -> tuple:
|
||||
try:
|
||||
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=300)
|
||||
return r.returncode == 0, (r.stdout + r.stderr).strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Command timed out after 300 seconds"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def get_plugin_version(config: dict) -> str:
|
||||
refs = config.get("version_refs", {})
|
||||
for ref in refs.get("plugin", {}).get("files", []):
|
||||
path = Path(ref["path"])
|
||||
if not path.exists():
|
||||
continue
|
||||
if path.suffix == ".json":
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
key = ref.get("key", "version")
|
||||
for k in key.split("."):
|
||||
data = data.get(k, {})
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
elif path.suffix in (".yaml", ".yml"):
|
||||
m = re.search(r"^version:\s*(.+)$", path.read_text(encoding="utf-8"), re.MULTILINE)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
elif path.suffix == ".xml":
|
||||
pattern = ref.get("pattern", "")
|
||||
if pattern and "{VERSION}" in pattern:
|
||||
regex = re.escape(pattern).replace(r"\{VERSION\}", r"([\d.]+(?:-\w+)?)")
|
||||
m = re.search(regex, path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
# fallback: match plugin id= line followed by version=
|
||||
m = re.search(r'id="[^"]+"\s+version="([^"]+)"', path.read_text(encoding="utf-8"))
|
||||
if m:
|
||||
return m.group(1)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def git_commit_and_tag(version: str, plugin_name: str) -> bool:
|
||||
print(f"\n[Git] Committing {plugin_name} v{version} ...")
|
||||
|
||||
ok, out = run(["git", "add", "-A"])
|
||||
if not ok:
|
||||
print(f" ERROR: git add failed:\n{out}")
|
||||
return False
|
||||
|
||||
ok, out = run(["git", "commit", "-m", f"chore: release {plugin_name} v{version}"])
|
||||
if not ok:
|
||||
print(f" ERROR: git commit failed:\n{out}")
|
||||
return False
|
||||
print(f" ✅ Committed")
|
||||
|
||||
ok, out = run(["git", "tag", f"v{version}"])
|
||||
if not ok:
|
||||
print(f" ERROR: git tag failed:\n{out}")
|
||||
return False
|
||||
print(f" ✅ Tagged v{version}")
|
||||
|
||||
ok, out = run(["git", "push", "origin", "HEAD"])
|
||||
print(f" {'✅' if ok else '⚠️ '} Push commits: {'OK' if ok else out}")
|
||||
|
||||
ok, out = run(["git", "push", "origin", f"v{version}"])
|
||||
print(f" {'✅' if ok else '⚠️ '} Push tag: {'OK' if ok else out}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def publish_npm() -> bool:
|
||||
print("\n[npm] Publishing ...")
|
||||
ok, out = run(["npm", "publish"])
|
||||
if ok:
|
||||
print(" ✅ Published to npm")
|
||||
return True
|
||||
print(f" ERROR: npm publish failed:\n{out}")
|
||||
return False
|
||||
|
||||
|
||||
def publish_dart() -> bool:
|
||||
print("\n[dart] Publishing to pub.dev ...")
|
||||
ok, out = run(["dart", "pub", "publish", "--force"])
|
||||
if ok:
|
||||
print(" ✅ Published to pub.dev")
|
||||
return True
|
||||
print(f" ERROR: dart pub publish failed:\n{out}")
|
||||
return False
|
||||
|
||||
|
||||
def prompt_dcloud(config: dict, plugin_name: str, version: str):
|
||||
pub = config.get("publish", {})
|
||||
paths = pub.get("dcloud_upload_paths", ["."])
|
||||
print(f"\n⚠️ [{plugin_name} v{version}] 需手动上传至 DCloud 插件市场")
|
||||
print(f" 上传地址: https://ext.dcloud.net.cn/")
|
||||
print(f" 请上传以下目录:")
|
||||
for p in paths:
|
||||
print(f" - {Path(p).resolve()}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Publish plugin")
|
||||
parser.add_argument("--skip-git", action="store_true", help="Skip git commit/tag/push")
|
||||
parser.add_argument("--skip-publish", action="store_true", help="Skip package registry publish")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config()
|
||||
plugin_name = config.get("plugin_name", "unknown")
|
||||
pub_type = config.get("publish", {}).get("type", "npm")
|
||||
version = get_plugin_version(config)
|
||||
|
||||
print(f"\n=== Publishing {plugin_name} v{version} ===")
|
||||
|
||||
if not args.skip_git:
|
||||
git_commit_and_tag(version, plugin_name)
|
||||
|
||||
if args.skip_publish:
|
||||
print("Skipping publish (--skip-publish)")
|
||||
return
|
||||
|
||||
if pub_type == "npm":
|
||||
publish_npm()
|
||||
elif pub_type == "dart":
|
||||
publish_dart()
|
||||
elif pub_type == "dcloud":
|
||||
prompt_dcloud(config, plugin_name, version)
|
||||
elif pub_type == "npm_and_dcloud":
|
||||
publish_npm()
|
||||
prompt_dcloud(config, plugin_name, version)
|
||||
|
||||
print(f"\n=== Done: {plugin_name} ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download and replace direct SDK files (for plugins that don't use maven/cocoapods)."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import fnmatch
|
||||
import tempfile
|
||||
import zipfile
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("ERROR: Missing dependencies. Run: pip3 install requests")
|
||||
sys.exit(1)
|
||||
|
||||
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
|
||||
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
|
||||
|
||||
# Download URL keys per sdk_type and platform
|
||||
# JCore has no standalone download — it's bundled inside JPush zip
|
||||
DOWNLOAD_KEYS = {
|
||||
"jpush": {"android": "android", "ios": "ios", "harmony": "hmos"},
|
||||
"jcore": {"android": "android", "ios": "ios"},
|
||||
}
|
||||
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_zip_url(sdk_type: str, platform: str) -> tuple[str, str]:
|
||||
"""Follow redirect chain to get the actual zip URL and available version."""
|
||||
key = DOWNLOAD_KEYS.get(sdk_type, {}).get(platform)
|
||||
if not key:
|
||||
return "", "unknown"
|
||||
|
||||
base_url = f"https://www.jiguang.cn/downloads/sdk/{key}"
|
||||
try:
|
||||
resp = requests.get(base_url, allow_redirects=True, headers=HEADERS, timeout=30)
|
||||
final_url = resp.url
|
||||
except requests.RequestException as e:
|
||||
print(f" ERROR: Failed to resolve download URL: {e}")
|
||||
return "", "unknown"
|
||||
|
||||
filename = Path(final_url.split("?")[0]).name
|
||||
match = re.search(r"(\d+\.\d+\.\d+)", filename)
|
||||
version = match.group(1) if match else "unknown"
|
||||
return final_url, version
|
||||
|
||||
|
||||
def extract_sdk_files(zip_url: str, dest_dir: Path, patterns: list) -> list[str]:
|
||||
"""Download zip and extract files/dirs matching patterns into dest_dir."""
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f" Downloading {zip_url.split('?')[0].split('/')[-1]} ...")
|
||||
try:
|
||||
resp = requests.get(zip_url, stream=True, headers=HEADERS, timeout=300)
|
||||
resp.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
print(f" ERROR: Download failed: {e}")
|
||||
return []
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
zip_path = tmp_path / "sdk.zip"
|
||||
|
||||
total = 0
|
||||
with open(zip_path, "wb") as f:
|
||||
for chunk in resp.iter_content(chunk_size=65536):
|
||||
f.write(chunk)
|
||||
total += len(chunk)
|
||||
print(f" Downloaded {total // 1024} KB")
|
||||
|
||||
with zipfile.ZipFile(zip_path) as zf:
|
||||
zf.extractall(tmp_path)
|
||||
|
||||
copied = _copy_matching(tmp_path, dest_dir, patterns)
|
||||
|
||||
return copied
|
||||
|
||||
|
||||
SKIP_DIRS = {"example", "demo", "__MACOSX", ".git"}
|
||||
|
||||
|
||||
def _copy_matching(src_root: Path, dest_dir: Path, patterns: list) -> list[str]:
|
||||
"""Walk src_root and copy files/dirs whose name matches any pattern.
|
||||
|
||||
Skips example/demo/__MACOSX directories and deduplicates by filename.
|
||||
"""
|
||||
copied = []
|
||||
visited_dirs = set()
|
||||
copied_names = set()
|
||||
|
||||
for item in sorted(src_root.rglob("*")):
|
||||
# Skip excluded directories and their contents
|
||||
if any(part in SKIP_DIRS for part in item.parts):
|
||||
continue
|
||||
# Skip if already copied as part of a parent directory
|
||||
if any(item.is_relative_to(d) for d in visited_dirs):
|
||||
continue
|
||||
|
||||
name = item.name
|
||||
if not any(fnmatch.fnmatch(name, p) for p in patterns):
|
||||
continue
|
||||
# Deduplicate by filename
|
||||
if name in copied_names:
|
||||
continue
|
||||
|
||||
dest = dest_dir / name
|
||||
|
||||
if item.is_dir():
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
shutil.copytree(item, dest)
|
||||
visited_dirs.add(item)
|
||||
print(f" Copied {name}/ → {dest_dir}")
|
||||
else:
|
||||
shutil.copy2(item, dest)
|
||||
print(f" Copied {name} → {dest_dir}")
|
||||
|
||||
copied.append(name)
|
||||
copied_names.add(name)
|
||||
|
||||
return copied
|
||||
|
||||
|
||||
def replace_from_local(user_path: str, dest_dir: Path, patterns: list) -> dict:
|
||||
"""Copy SDK files from a user-provided local path."""
|
||||
src = Path(user_path)
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if src.is_file() and src.suffix == ".zip":
|
||||
print(f" Extracting {src.name} ...")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with zipfile.ZipFile(src) as zf:
|
||||
zf.extractall(tmp)
|
||||
copied = _copy_matching(Path(tmp), dest_dir, patterns)
|
||||
else:
|
||||
copied = _copy_matching(src, dest_dir, patterns)
|
||||
|
||||
return {"status": "updated", "source": "user_provided", "files": copied}
|
||||
|
||||
|
||||
def handle_platform(platform: str, sdk_type: str, target_version: str,
|
||||
direct_cfg: dict, user_sdk_path: str | None) -> dict:
|
||||
print(f"\n=== {platform.upper()} SDK (target: v{target_version}) ===")
|
||||
dest_dir = Path(direct_cfg["dest_dir"])
|
||||
patterns = direct_cfg.get("file_patterns", ["*.aar", "*.jar", "*.a", "*.framework", "*.xcframework"])
|
||||
|
||||
if user_sdk_path:
|
||||
result = replace_from_local(user_sdk_path, dest_dir, patterns)
|
||||
return {"platform": platform, **result}
|
||||
|
||||
zip_url, available_version = get_zip_url(sdk_type, platform)
|
||||
|
||||
if not zip_url:
|
||||
return {"platform": platform, "status": "error", "reason": "Could not resolve download URL"}
|
||||
|
||||
# For jcore, the SDK is bundled inside the JPush zip — skip zip-filename version check.
|
||||
# For jpush/harmony, zip filename carries the SDK version directly.
|
||||
if sdk_type != "jcore" and available_version != target_version.lstrip("v"):
|
||||
msg = (f"Latest available version is {available_version}, "
|
||||
f"target {target_version} not yet released. "
|
||||
f"Re-run with --{platform}-sdk-path /path/to/sdk when available.")
|
||||
print(f" ⚠️ {msg}")
|
||||
return {"platform": platform, "status": "version_mismatch",
|
||||
"available": available_version, "target": target_version, "reason": msg}
|
||||
|
||||
copied = extract_sdk_files(zip_url, dest_dir, patterns)
|
||||
if copied:
|
||||
return {"platform": platform, "status": "updated", "source": "auto", "files": copied}
|
||||
else:
|
||||
return {"platform": platform, "status": "error", "reason": "No matching files found in zip"}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Download and replace direct SDK files")
|
||||
parser.add_argument("--android", help="Android SDK target version")
|
||||
parser.add_argument("--ios", help="iOS SDK target version")
|
||||
parser.add_argument("--harmony", help="HarmonyOS SDK target version")
|
||||
parser.add_argument("--android-sdk-path", help="Local path to Android SDK (zip or dir)")
|
||||
parser.add_argument("--ios-sdk-path", help="Local path to iOS SDK (zip or dir)")
|
||||
parser.add_argument("--harmony-sdk-path", help="Local path to HarmonyOS SDK (zip or dir)")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config()
|
||||
sdk_type = config.get("sdk_type", "jpush")
|
||||
direct_sdk = config.get("direct_sdk", {})
|
||||
|
||||
if not direct_sdk:
|
||||
print("INFO: This plugin uses maven/cocoapods. No direct SDK files to manage.")
|
||||
return
|
||||
|
||||
results = []
|
||||
platforms = [
|
||||
("android", args.android, args.android_sdk_path),
|
||||
("ios", args.ios, args.ios_sdk_path),
|
||||
("harmony", args.harmony, args.harmony_sdk_path),
|
||||
]
|
||||
|
||||
for platform, version, user_path in platforms:
|
||||
if version and direct_sdk.get(platform):
|
||||
results.append(handle_platform(platform, sdk_type, version,
|
||||
direct_sdk[platform], user_path))
|
||||
|
||||
with open(".claude/skills/update-sdk/scripts/.sdk_download_result.json", "w") as f:
|
||||
json.dump(results, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print("\n=== SDK download summary ===")
|
||||
for r in results:
|
||||
icon = "✅" if r["status"] == "updated" else "⚠️ "
|
||||
detail = f" ({len(r.get('files', []))} files)" if r.get("files") else ""
|
||||
print(f" {icon} {r['platform']}: {r['status']}{detail}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -67,3 +67,8 @@ ionic/example/src/app/app\.scss
|
||||
ionic/example/src/app/main\.ts
|
||||
|
||||
# End of https://www.gitignore.io/api/macos,apachecordova
|
||||
|
||||
# Claude update-sdk skill temp files
|
||||
.claude/skills/update-sdk/scripts/.sdk_download_result.json
|
||||
.claude/skills/update-sdk/scripts/.changelog_cache.json
|
||||
.claude/settings.local.json
|
||||
Reference in New Issue
Block a user