feat(pkg-update): 新增版本改寫工具並改用工具呼叫

What: 新增 tools/apply-version.sh,依 ecosystem 改寫專案的版本來源檔(package.json / requirements.txt / pyproject.toml / *.csproj),把指定套件釘選到指定版本;skills/pkg-update/SKILL.md 的改寫步驟改成呼叫這支工具;README.md 補上工具說明;plugin.json、.claude-plugin/plugin.json、.codex-plugin/plugin.json 版本號由 0.0.1 升到 0.0.2。
Why: jsc-meta:skill-check 稽核發現 SKILL.md 用文字描述改寫邏輯,不符合「邏輯放工具、SKILL.md 只放步驟」的準則,需要把改寫邏輯抽成獨立工具。
How: 依 tools/list-packages.sh 已支援的四種生態(nodejs 的 package.json、python 的 requirements.txt 或 pyproject.toml、dotnet 的 *.csproj)撰寫對應的 parse 與 rewrite 邏輯,找不到檔案時 exit 3、找不到套件時 exit 4;SKILL.md 步驟改成呼叫 `tools/apply-version.sh {ecosystem} {project dir} {name} {version}`;三份 manifest 版本號同步升級。
Who: jsc-pkg 套件更新流程,影響 pkg-update 技能與所有安裝 jsc-pkg 的使用者。
This commit is contained in:
2026-08-24 14:48:30 +08:00
parent 3e3e1e588f
commit 5ae3611960
6 changed files with 135 additions and 4 deletions
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env sh
# apply-version.sh — 改寫版本來源檔案,把套件釘選到指定版本。
# 用法: apply-version.sh <ecosystem> <project-dir> <name> <version>
# ecosystem: nodejs | python | dotnet
# 對應檔案(與 list-packages.sh 相同的解析邏輯):
# nodejs → package.json(dependencies / devDependencies)
# python → requirements.txt(優先)或 pyproject.toml 的 [project] dependencies
# dotnet → *.csproj 的 PackageReference
# 找不到對應檔案 exit 3;檔案中找不到該套件 exit 4
set -u
eco="${1:?ecosystem required (nodejs|python|dotnet)}"
DIR="${2:?project dir required}"
name="${3:?package name required}"
version="${4:?version required}"
case "$eco" in
nodejs)
[ -f "$DIR/package.json" ] || { echo "package.json not found in $DIR" >&2; exit 3; }
python3 - "$DIR/package.json" "$name" "$version" <<'EOF'
import json,sys
path,name,version=sys.argv[1],sys.argv[2],sys.argv[3]
d=json.load(open(path,encoding="utf-8"))
found=False
for k in ("dependencies","devDependencies"):
if name in (d.get(k) or {}):
d[k][name]=version
found=True
if not found:
sys.exit(4)
with open(path,"w",encoding="utf-8") as f:
json.dump(d,f,indent=2,ensure_ascii=False)
f.write("\n")
EOF
rc=$?
[ "$rc" -eq 0 ] || { echo "$name not found in $DIR/package.json" >&2; exit "$rc"; }
;;
python)
if [ -f "$DIR/requirements.txt" ]; then
python3 - "$DIR/requirements.txt" "$name" "$version" <<'EOF'
import re,sys
path,name,version=sys.argv[1],sys.argv[2],sys.argv[3]
pat=re.compile(r"([A-Za-z0-9_.\-\[\]]+)\s*(?:(?:==|>=|<=|~=|!=|>|<)\s*([^,;\s]+))?")
lname=name.split("[")[0].lower()
lines=open(path,encoding="utf-8").readlines()
out=[]
found=False
for line in lines:
body=line.split("#")[0].strip()
m=pat.match(body)
if m and m.group(1).split("[")[0].lower()==lname:
out.append(f"{m.group(1)}=={version}\n")
found=True
else:
out.append(line)
if not found:
sys.exit(4)
open(path,"w",encoding="utf-8").writelines(out)
EOF
rc=$?
[ "$rc" -eq 0 ] || { echo "$name not found in $DIR/requirements.txt" >&2; exit "$rc"; }
elif [ -f "$DIR/pyproject.toml" ]; then
python3 - "$DIR/pyproject.toml" "$name" "$version" <<'EOF'
import re,sys
path,name,version=sys.argv[1],sys.argv[2],sys.argv[3]
pat=re.compile(r"([A-Za-z0-9_.\-\[\]]+)\s*(?:(?:==|>=|<=|~=|!=|>|<)\s*([^,;\s\"']+))?")
lname=name.split("[")[0].lower()
lines=open(path,encoding="utf-8").readlines()
out=[]
found=False
for line in lines:
m=re.search(r'"([^"]+)"', line)
if m:
dep=m.group(1)
dm=pat.match(dep)
if dm and dm.group(1).split("[")[0].lower()==lname:
newdep=f"{dm.group(1)}=={version}"
line=line.replace(dep,newdep,1)
found=True
out.append(line)
if not found:
sys.exit(4)
open(path,"w",encoding="utf-8").writelines(out)
EOF
rc=$?
[ "$rc" -eq 0 ] || { echo "$name not found in $DIR/pyproject.toml" >&2; exit "$rc"; }
else
echo "requirements.txt or pyproject.toml not found in $DIR" >&2
exit 3
fi
;;
dotnet)
projs=$(find "$DIR" -maxdepth 3 -name '*.csproj' 2>/dev/null)
[ -n "$projs" ] || { echo "*.csproj not found in $DIR" >&2; exit 3; }
found=0
for proj in $projs; do
python3 - "$proj" "$name" "$version" <<'EOF'
import re,sys
path,name,version=sys.argv[1],sys.argv[2],sys.argv[3]
text=open(path,encoding="utf-8").read()
hit=[False]
def repl(m):
tag=m.group(0)
inc=re.search(r'Include="([^"]+)"',tag)
if inc and inc.group(1)==name:
hit[0]=True
if re.search(r'Version="[^"]*"',tag):
tag=re.sub(r'Version="[^"]*"', 'Version="%s"'%version, tag)
elif tag.endswith("/>"):
tag=tag[:-2]+' Version="%s" />'%version
else:
tag=tag[:-1]+' Version="%s">'%version
return tag
new_text=re.sub(r"<PackageReference\b[^>]*>", repl, text)
if not hit[0]:
sys.exit(4)
open(path,"w",encoding="utf-8").write(new_text)
EOF
if [ $? -eq 0 ]; then
found=1
break
fi
done
[ "$found" -eq 1 ] || { echo "PackageReference $name not found under $DIR" >&2; exit 4; }
;;
*) echo "unknown ecosystem: $eco" >&2; exit 2 ;;
esac
exit 0