From ea11ca0590c3b296d3d28e1b444f0f5390639e92 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(pkg):=20=E8=A3=9C=E9=BD=8A=E7=A8=BD?= =?UTF-8?q?=E6=A0=B8=E7=BC=BA=E5=A4=B1=E4=B8=A6=E4=BF=AE=E6=8E=89=E8=AD=B7?= =?UTF-8?q?=E6=AC=84=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、 把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。 Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。 完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。 How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有 documented exit codes,並以真實執行驗證每條路徑。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 --- skills/pkg-update/SKILL.md | 55 ++++++++++++++++++----- tools/apply-version.sh | 35 +++++++++++---- tools/build-test.sh | 91 ++++++++++++++++++++++++++++++++++++++ tools/install-deps.sh | 73 ++++++++++++++++++++++++++++++ tools/latest-version.sh | 28 +++++++++--- tools/list-packages.sh | 23 ++++++++-- 6 files changed, 277 insertions(+), 28 deletions(-) create mode 100755 tools/build-test.sh create mode 100755 tools/install-deps.sh diff --git a/skills/pkg-update/SKILL.md b/skills/pkg-update/SKILL.md index 839d2a5..345cdff 100644 --- a/skills/pkg-update/SKILL.md +++ b/skills/pkg-update/SKILL.md @@ -5,18 +5,49 @@ description: Update every external package of a project to its latest stable ver # pkg-update — batch-update packages +## Exit-code routes + +Every tool exit code below has exactly one route. Three routes exist: + +- **skip** — record the reason, keep going. +- **stop** — report and end the run. Change no file, and never enter step 5. +- **revert** — go to step 5, which reverts the working tree. + +Only a real install, build or test failure takes the revert route. A missing input, a bad argument, an unknown ecosystem or a missing command is a broken call or a broken toolchain, so it takes the stop route. + +One number carries one meaning across all five tools. Exit 6 is always a missing project directory and always stops the run — never fold it into the exit 3 skip, or a bad path hides as "nothing to do". + +| Tool | 0 | 1 (bad argument count) | 2 (unknown ecosystem) | 3 (no source file) | 4 | 5 | 6 (project dir not found) | other | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `list-packages.sh` | continue | stop | — | — | stop (python3 missing) | — | stop | stop | +| `latest-version.sh` | continue | stop | stop | — | skip (not in registry) | stop (curl or python3 missing) | — (takes no project dir) | stop | +| `apply-version.sh` | continue | stop | stop | skip | skip (package absent from the file) | stop (python3 missing) | stop | stop | +| `install-deps.sh` | continue | stop | stop | skip | stop (npm, pip or dotnet missing) | — | stop | **revert** | +| `build-test.sh` | continue | stop | stop | — (uses 5 instead) | stop (npm, python3, pytest or dotnet missing) | ask the user | stop | **revert** | + +Exit 4 and exit 5 are the two codes whose meaning depends on the tool, so read them off this table rather than from memory. + ## Steps -1. Run `tools/list-packages.sh {project dir}` to get every external package (ecosystem, name, current version). -2. Confirm the working tree is clean: stop and report when uncommitted changes exist, so the revert cannot destroy them. +1. Run `tools/list-packages.sh {project dir}` to get every external package (ecosystem, name, current version). Route the exit code per the table above. Step 1 is done only when the tool exited 0 and at least one row came back, with an ecosystem, a name and a current version in every row. Zero rows → stop and report the project directory you scanned plus the supported ecosystems (nodejs / python / dotnet), and run no further step. +2. Prove the project directory is a git repository with a clean tree. Run this before any step changes a file: + 1. Run `git -C {project dir} rev-parse --git-dir`. Exit 0 is required to continue, because step 5's revert is impossible outside a git repository. Any other exit code → stop and report the directory, and run no further step. + 2. Run `git -C {project dir} status --porcelain`. Continuing requires that it prints **nothing at all**. Any output stops the run and reports that output, **including `??` untracked lines**: step 5 runs `git clean -fd`, which deletes untracked files with no reflog and no way back. + 3. Step 2 is done only when `rev-parse --git-dir` exited 0 and `status --porcelain` printed zero bytes. Report both results, then enter step 3. 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. 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). - - python: `pytest` (when tests exist). - - dotnet: `dotnet build` plus `dotnet test`. - - When the commands cannot be inferred, ask the user per the `jsc-ask:ask` rules. -5. Build or tests fail → revert every change (`git checkout -- .` and clean untracked lock changes), then report the failing packages with an error summary. -6. Success → report the update list (package, old version, new version) and hand off to `jsc-git:commit`. + 1. For every package, run `tools/latest-version.sh {ecosystem} {name}` for the latest stable version, then `tools/apply-version.sh {ecosystem} {project dir} {name} {version}` to rewrite the version source file (package.json / requirements.txt / pyproject.toml / *.csproj). This substep is done when every package of the ecosystem carries either an applied version or a skip reason from substep 2. + 2. Route every exit code per the table above. The skip route covers `latest-version.sh` exit 4 (package not found in the registry), `apply-version.sh` exit 3 (no version source file) and `apply-version.sh` exit 4 (package absent from the file): record the package and the reason, move on to the next package, and never abort the whole ecosystem. The stop route covers exit 1, exit 2, `latest-version.sh` or `apply-version.sh` exit 5 (a required command is missing) and `apply-version.sh` exit 6 (project directory not found): report the code and end the run without entering step 5. This substep is done when every exit code seen has taken exactly one route. + 3. Run `tools/install-deps.sh {ecosystem} {project dir}` once, after every package of that ecosystem is applied. Exit 0 means the install finished. Exit 3 means this ecosystem has no dependency source file, which is the same documented skip as substep 2: record the ecosystem and the reason, and install nothing. Exit 1, exit 2, exit 4 and exit 6 take the stop route — exit 6 is a missing project directory, so report it instead of skipping it. Only another non-zero code is a real install failure and goes to step 5. This substep is done when the exit code is recorded together with the route it took. + 4. The sub agent returns one row per package from substep 1: name, old version, new version — or `skipped` plus the reason. It also returns the `install-deps.sh` exit code and its route. The ecosystem is done only when every package appears in exactly one row. +4. Run `tools/build-test.sh {ecosystem} {project dir}` for every ecosystem and route on the exit code: + - 0 → this ecosystem passed. + - 1, 2, 4 or 6 → stop route. Report the code and the missing argument, ecosystem, command or directory, and enter no further step. Exit 4 means the toolchain is broken, not that the packages are broken, so reverting would destroy files for nothing. + - 5 → no build or test command could be inferred. Ask the user for the build command and the test command per the `jsc-ask:ask` rules, run the answers in that order, and take their exit codes as this ecosystem's result: all 0 → passed; any non-zero → step 5. + - any other code → a real build or test failure. Go to step 5. + + Step 4 is done only when every ecosystem reached exit 0, from the tool or from the user's own commands. +5. A real install, build or test failure reached this step → revert: + 1. Run `git -C {project dir} checkout -- .` to restore every tracked file to HEAD. + 2. Run `git -C {project dir} clean -fd` to remove the untracked files and directories this run created, such as a new lock file. Gitignored paths survive `-fd` on purpose: `node_modules`, `__pycache__`, `bin/` and `obj/` stay behind, so restoring install output is out of scope for this skill. Do not reach for `-fdx` — it deletes far more than this run created. + 3. The revert is done only when `git -C {project dir} status --porcelain` prints nothing. Report the failing packages with an error summary after that check passes. +6. Success → report the update list (package, old version, new version), plus every skip and its reason, and hand off to `jsc-git:commit`. Step 6 is done when the hand-off is made and every package from step 1 appears either as an update or as a skip. diff --git a/tools/apply-version.sh b/tools/apply-version.sh index c519d8c..d8605dc 100755 --- a/tools/apply-version.sh +++ b/tools/apply-version.sh @@ -1,17 +1,36 @@ #!/usr/bin/env sh # apply-version.sh — 改寫版本來源檔案,把套件釘選到指定版本。 -# 用法: apply-version.sh -# ecosystem: nodejs | python | dotnet -# 對應檔案(與 list-packages.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 +# 結束碼(慣例見 README「工具」章的結束碼總表): +# 0 改寫成功 +# 1 參數個數不對 +# 2 ecosystem 不認識 +# 3 找不到對應的版本來源檔案——呼叫方跳過這個套件 +# 4 檔案中找不到該套件——呼叫方跳過這個套件 +# 5 需要的指令不存在(python3) +# 6 找不到專案目錄——呼叫方停手,不要當成跳過 set -u -eco="${1:?ecosystem required (nodejs|python|dotnet)}" -DIR="${2:?project dir required}" -name="${3:?package name required}" -version="${4:?version required}" + +if [ "$#" -ne 4 ]; then + echo "用法:apply-version.sh " >&2 + exit 1 +fi + +eco="$1" +DIR="$2" +name="$3" +version="$4" + +# 先擋找不到專案目錄。少了這道,壞路徑會被報成「沒有版本來源檔案」,然後被當成跳過藏起來。 +[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } + +# 三種來源檔案都靠 python3 改寫。少了 python3 會被誤判成檔案中找不到該套件,所以先擋。 +command -v python3 >/dev/null 2>&1 || { echo "python3 not found" >&2; exit 5; } case "$eco" in nodejs) diff --git a/tools/build-test.sh b/tools/build-test.sh new file mode 100755 index 0000000..d7fcfea --- /dev/null +++ b/tools/build-test.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env sh +# build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。 +# 用法:build-test.sh +# ecosystem:nodejs | python | dotnet +# 偵測方式: +# nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有) +# python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py +# dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet +# 輸出:底層指令的原始輸出。 +# 結束碼(慣例見 README「工具」章的結束碼總表): +# 0 建置與測試都通過 +# 1 參數個數不對 +# 2 ecosystem 不認識 +# 4 需要的指令不存在(npm、python3、pytest、dotnet) +# 5 推不出任何建置或測試指令——呼叫方要改問使用者 +# 6 找不到專案目錄——呼叫方停手 +# 其他 建置或測試失敗,帶回失敗指令的結束碼 +# 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。 +set -u + +if [ "$#" -ne 2 ]; then + echo "用法:build-test.sh " >&2 + exit 1 +fi + +eco="$1" +DIR="$2" + +NO_COMMAND=5 + +[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } + +case "$eco" in + nodejs) + [ -f "$DIR/package.json" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } + # package.json 靠 python3 解析。少了 python3 會解出空字串,被誤判成推不出指令,所以先擋。 + command -v python3 >/dev/null 2>&1 || { echo "python3 not found" >&2; exit 4; } + scripts=$(python3 - "$DIR/package.json" <<'EOF' +import json,sys +try: + d=json.load(open(sys.argv[1],encoding="utf-8")) +except Exception: + sys.exit(0) +s=(d.get("scripts") or {}) +if s.get("build"): print("build") +t=s.get("test") or "" +if t and "no test specified" not in t: print("test") +EOF +) + [ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } + command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; } + for s in $scripts; do + ( cd "$DIR" && npm run --if-present "$s" ) || exit $? + done + exit 0 + ;; + + python) + has_tests=0 + for f in pytest.ini setup.cfg tox.ini pyproject.toml; do + if [ -f "$DIR/$f" ] && grep -q 'pytest' "$DIR/$f" 2>/dev/null; then has_tests=1; fi + done + [ -d "$DIR/tests" ] && has_tests=1 + if [ "$has_tests" -eq 0 ]; then + found=$(find "$DIR" -maxdepth 3 -name 'test_*.py' 2>/dev/null | head -n 1) + [ -n "$found" ] && has_tests=1 + fi + [ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } + if command -v pytest >/dev/null 2>&1; then + ( cd "$DIR" && pytest ) + exit $? + elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then + ( cd "$DIR" && python3 -m pytest ) + exit $? + else + echo "pytest not found" >&2 + exit 4 + fi + ;; + + dotnet) + projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null) + [ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } + command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; } + ( cd "$DIR" && dotnet build ) || exit $? + ( cd "$DIR" && dotnet test ) || exit $? + exit 0 + ;; + + *) echo "unknown ecosystem: $eco" >&2; exit 2 ;; +esac diff --git a/tools/install-deps.sh b/tools/install-deps.sh new file mode 100755 index 0000000..48027b5 --- /dev/null +++ b/tools/install-deps.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env sh +# install-deps.sh — 重新解析並安裝專案的相依套件。 +# 用法:install-deps.sh +# ecosystem:nodejs | python | dotnet +# 對應動作: +# nodejs → npm install +# python → pip install -r requirements.txt(優先)或 pip install -e .(pyproject.toml) +# dotnet → dotnet restore +# 輸出:底層指令的原始輸出。 +# 檢查順序:先確認相依來源檔案存在,才檢查指令存在。順序顛倒會把「沒有來源檔案」報成「指令不存在」。 +# 結束碼(慣例見 README「工具」章的結束碼總表): +# 0 安裝成功 +# 1 參數個數不對 +# 2 ecosystem 不認識 +# 3 該 ecosystem 沒有相依來源檔案——呼叫方跳過這個 ecosystem +# 4 需要的指令不存在(npm、pip、dotnet) +# 6 找不到專案目錄——呼叫方停手,不要當成跳過 +# 其他 底層指令的結束碼,代表安裝失敗 +set -u + +if [ "$#" -ne 2 ]; then + echo "用法:install-deps.sh " >&2 + exit 1 +fi + +eco="$1" +DIR="$2" + +[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } + +case "$eco" in + nodejs) + [ -f "$DIR/package.json" ] || { echo "package.json not found in $DIR" >&2; exit 3; } + command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; } + ( cd "$DIR" && npm install ) + exit $? + ;; + + python) + if [ -f "$DIR/requirements.txt" ]; then + target="-r requirements.txt" + elif [ -f "$DIR/pyproject.toml" ]; then + target="-e ." + else + echo "requirements.txt or pyproject.toml not found in $DIR" >&2 + exit 3 + fi + pip="" + for c in pip3 pip; do + if command -v "$c" >/dev/null 2>&1; then pip="$c"; break; fi + done + if [ -z "$pip" ]; then + if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then + pip="python3 -m pip" + else + echo "pip not found" >&2 + exit 4 + fi + fi + ( cd "$DIR" && $pip install $target ) + exit $? + ;; + + dotnet) + projs=$(find "$DIR" -maxdepth 3 \( -name '*.csproj' -o -name '*.sln' \) 2>/dev/null) + [ -n "$projs" ] || { echo "*.csproj or *.sln not found in $DIR" >&2; exit 3; } + command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; } + ( cd "$DIR" && dotnet restore ) + exit $? + ;; + + *) echo "unknown ecosystem: $eco" >&2; exit 2 ;; +esac diff --git a/tools/latest-version.sh b/tools/latest-version.sh index 81a3e98..01a82b3 100755 --- a/tools/latest-version.sh +++ b/tools/latest-version.sh @@ -1,11 +1,29 @@ #!/usr/bin/env sh # latest-version.sh — 查詢套件最新且穩定的版本號。 -# 用法: latest-version.sh -# ecosystem: nodejs | python | dotnet -# 輸出: 版本號一行;查不到 exit 4 +# 用法:latest-version.sh +# ecosystem:nodejs | python | dotnet +# 輸出:版本號一行。 +# 結束碼(慣例見 README「工具」章的結束碼總表): +# 0 查到版本號 +# 1 參數個數不對 +# 2 ecosystem 不認識 +# 4 登錄站上查不到這個套件——呼叫方跳過這個套件 +# 5 需要的指令不存在(curl、python3) +# 本工具不收專案目錄,所以沒有 6。 set -u -eco="${1:?ecosystem required (nodejs|python|dotnet)}" -name="${2:?package name required}" + +if [ "$#" -ne 2 ]; then + echo "用法:latest-version.sh " >&2 + exit 1 +fi + +eco="$1" +name="$2" + +# 回應靠 curl 取得、靠 python3 解析。少了任一支會解出空字串,被誤判成查不到套件,所以先擋。 +for c in curl python3; do + command -v "$c" >/dev/null 2>&1 || { echo "$c not found" >&2; exit 5; } +done case "$eco" in nodejs) diff --git a/tools/list-packages.sh b/tools/list-packages.sh index ee12fc1..7488f71 100755 --- a/tools/list-packages.sh +++ b/tools/list-packages.sh @@ -1,11 +1,28 @@ #!/usr/bin/env sh # list-packages.sh — 列出專案的所有外部套件與目前版本。 -# 用法: list-packages.sh [專案目錄](預設目前目錄) -# 輸出(TSV): ecosystemnamecurrent -# 支援: nodejs(package.json)、python(requirements.txt / pyproject.toml)、dotnet(*.csproj) +# 用法:list-packages.sh [專案目錄](預設目前目錄) +# 輸出(TSV):ecosystemnamecurrent +# 支援:nodejs(package.json)、python(requirements.txt / pyproject.toml)、dotnet(*.csproj) +# 結束碼(慣例見 README「工具」章的結束碼總表): +# 0 掃描完成(沒有套件時輸出零行) +# 1 參數個數不對 +# 4 需要的指令不存在(python3) +# 6 找不到專案目錄——呼叫方停手 set -u + +if [ "$#" -gt 1 ]; then + echo "用法:list-packages.sh [專案目錄]" >&2 + exit 1 +fi + DIR="${1:-.}" +# 先擋找不到專案目錄。少了這道,壞路徑會輸出零行,被誤判成專案沒有套件。 +[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } + +# 四種來源檔案都靠 python3 解析。少了 python3 會輸出零行,被誤判成專案沒有套件,所以先擋。 +command -v python3 >/dev/null 2>&1 || { echo "python3 not found" >&2; exit 4; } + # nodejs: package.json 的 dependencies / devDependencies if [ -f "$DIR/package.json" ]; then python3 - "$DIR/package.json" <<'EOF'