diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5352321..f860340 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-pkg", - "version": "0.0.1", + "version": "0.0.2", "description": "套件批次更新(nodejs/python/dotnet),失敗還原", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 7df50c9..96a1b43 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-pkg", - "version": "0.0.1", + "version": "0.0.2", "description": "套件批次更新(nodejs/python/dotnet),失敗還原", "skills": "./skills" } diff --git a/README.md b/README.md index 8336161..6d92674 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/jsc.git),安 | --- | --- | | `tools/list-packages.sh` | 偵測專案類型並列出所有外部套件與目前版本(TSV:ecosystem / name / current) | | `tools/latest-version.sh` | `latest-version.sh ` 查最新穩定版(npm / pypi / nuget),查無 exit 4 | +| `tools/apply-version.sh` | `apply-version.sh ` 改寫版本來源檔案,把套件釘選到指定版本 | ## Skills 目錄 diff --git a/plugin.json b/plugin.json index 0bb0358..31cbb33 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-pkg", - "version": "0.0.1", + "version": "0.0.2", "description": "套件批次更新(nodejs/python/dotnet),失敗還原", "skills": "./skills/" } diff --git a/skills/pkg-update/SKILL.md b/skills/pkg-update/SKILL.md index 03a5a3a..839d2a5 100644 --- a/skills/pkg-update/SKILL.md +++ b/skills/pkg-update/SKILL.md @@ -11,7 +11,7 @@ description: Update every external package of a project to its latest stable ver 2. Confirm the working tree is clean: stop and report when uncommitted changes exist, so the revert cannot destroy them. 3. Update ecosystem by ecosystem. This step **MUST run as a sub agent** (one sub agent per ecosystem): 1. For every package, run `tools/latest-version.sh {ecosystem} {name}` to get the latest stable version. - 2. Rewrite the version source file (package.json / requirements.txt / pyproject.toml / *.csproj). + 2. Run `tools/apply-version.sh {ecosystem} {project dir} {name} {version}` to rewrite the version source file (package.json / requirements.txt / pyproject.toml / *.csproj). 3. Re-resolve and install (`npm install` / `pip install -r` / `dotnet restore`). 4. Run build and tests: - nodejs: `npm run build` (if present) plus `npm test` (if present). diff --git a/tools/apply-version.sh b/tools/apply-version.sh new file mode 100755 index 0000000..c519d8c --- /dev/null +++ b/tools/apply-version.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env sh +# apply-version.sh — 改寫版本來源檔案,把套件釘選到指定版本。 +# 用法: apply-version.sh +# 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"]*>", 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