453dec60b1
scripts/install.sh 與 scripts/update.sh 的更新來源指向已被 GitHub suspend 的 uncle6me-web 帳號,實測全部 404 ⇒ 自動更新靜默失效。改成與 system-dev-template 本體、以及本 repo 現行世代 system-dev/scripts/ 一致的正解:預設公開 GitHub youlinhsieh/system-dev-template,可用 TEMPLATE_SOURCE 環境變數覆寫,不改檔。 同時補上 looks_like_error_page 判斷(沿用 template repo 已修好的寫法):curl 對 404 常回傳非空的錯誤頁內容,只判斷「非空」會把它當成正常檔案寫入且不報錯。 現在錯誤頁會被偵測、檔案不寫入、FAILED 清單於結尾列出、腳本以非零狀態碼結束。 實測: - 修正後來源多個路徑回真實 HTTP 200(CLAUDE.md/VERSION/self-update 兩支腳本)。 - 刻意把 TEMPLATE_SOURCE 指回死帳號重跑兩支腳本:全部項目進 FAILED 清單、 無任何檔案被錯誤頁污染、exit code 1。 - 兩支腳本 bash -n 語法檢查通過。 已知缺口(不在本次範圍,留待另決):template repo 內部檔案佈局已從 .claude/wiki/、docs/ 搬到 system-dev/ 前綴,這兩支舊世代腳本仍用舊路徑, 其中一部分檔案(wiki 範本、docs/README、SDD 範本等)在新帳號上仍 404—— 現在會清楚回報失敗而不是靜默吞掉,但尚未逐一改點新路徑。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
276 lines
13 KiB
Bash
Executable File
276 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
# system-dev-template updater
|
||
# 已安裝舊版的人,一鍵更新到新版。
|
||
#
|
||
# 核心安全原則:只覆蓋「模板/邏輯檔」,絕不碰「使用者資料檔」。
|
||
# ✅ 可覆蓋:hooks/*.sh、commands/*.md、TEMPLATE-*、wiki/INDEX.md
|
||
# ——這些由模板維護,使用者不會手改,新版直接換掉。
|
||
# 🔒 絕不碰:wiki/status.md、mistakes.md、decisions-summary.md、TAXONOMY.md、.wikiignore、
|
||
# settings.json、CLAUDE.md
|
||
# ——這些是使用者自己填的內容,覆蓋=清空他的記憶與設定。
|
||
#
|
||
# 「第一次更新」的雞生蛋問題:
|
||
# 舊版本機沒有 update.sh。所以第一次靠 README 那行 curl 從遠端抓這支腳本來跑。
|
||
# 跑完它會把自己也更新進 scripts/update.sh,之後就能直接跑本機的 `bash scripts/update.sh`。
|
||
|
||
set -euo pipefail
|
||
|
||
# ── i18n:依 locale 選語言,預設英文(curl | bash 常為 LANG=C)──
|
||
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
|
||
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
|
||
*) IS_ZH="no" ;;
|
||
esac
|
||
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
|
||
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||
|
||
# 來源預設=公開 GitHub(leo 2026-07-21:「要發佈的正稿,從頭就不要用奇怪的網址,
|
||
# 以免改來改去」)。舊的 uncle6me-web 帳號已被 suspend(讀 404)=自動更新靜默失效,
|
||
# 這顆雷已在 system-dev-template 本體修過(正解=改指 youlinhsieh 這個公開帳號),
|
||
# 這裡只是把同一個修法補進 Arcrun 這份沒跟到的 copy。
|
||
# 內部要指私有草稿源時用環境變數覆寫,不改檔:
|
||
# TEMPLATE_SOURCE=https://<私有 raw base> bash scripts/update.sh
|
||
REPO_RAW="${TEMPLATE_SOURCE:-https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main}"
|
||
TEMPLATE_URL="$REPO_RAW/template"
|
||
|
||
UPDATED=()
|
||
KEPT=()
|
||
NEW=()
|
||
TEMPLATED=()
|
||
FAILED=()
|
||
|
||
# 404 頁面常常「非空」(GitHub raw 對不存在的路徑回 "404: Not Found"),
|
||
# 只判斷 [ -s file ] 會把錯誤頁內容當成檔案寫進去且完全不報錯(template issue #13 撞過的雷)。
|
||
looks_like_error_page() {
|
||
local f="$1"
|
||
[ -s "$f" ] || return 0
|
||
if [ "$(wc -l < "$f" | tr -d ' ')" -le 2 ] && head -c 200 "$f" | grep -qiE '40[0-9]|not found|<html'; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ── 版本比對:先看本機 vs 遠端,給使用者「值不值得更新」的判斷 ──
|
||
LOCAL_VER="$(tn '(未知)' '(unknown)')"
|
||
[ -f ".claude/VERSION" ] && LOCAL_VER="$(tr -d '[:space:]' < .claude/VERSION)"
|
||
REMOTE_VER="$(curl -sSL "$TEMPLATE_URL/.claude/VERSION" 2>/dev/null | tr -d '[:space:]' || echo '')"
|
||
# 容錯:curl 對 404 會把「404: Not Found」當內容輸出(非空),舊版直接把它寫進 REMOTE_VER
|
||
# 拿去跟本機版本比對,比對邏輯不會報錯、只會靜靜判斷「版本不同」或誤判「已最新」。
|
||
# 這裡驗證必須像版號(X.Y.Z),否則一律視為取不到。
|
||
case "$REMOTE_VER" in
|
||
[0-9]*.[0-9]*.[0-9]*) : ;; # 形如 1.9.0 → 合法
|
||
*) REMOTE_VER="" ;; # 404 / HTML 錯誤頁 / 其他 → 當作沒抓到
|
||
esac
|
||
|
||
echo ""
|
||
echo "🔄 system-dev-template updater"
|
||
echo "================================="
|
||
t " 本機版本:${LOCAL_VER}" " Local version: ${LOCAL_VER}"
|
||
t " 最新版本:${REMOTE_VER:-取不到(檢查網路)}" \
|
||
" Latest version: ${REMOTE_VER:-unavailable (check network)}"
|
||
echo ""
|
||
|
||
if [ -z "$REMOTE_VER" ]; then
|
||
t "❌ 取不到遠端版本,可能是網路問題。請稍後再試。" \
|
||
"❌ Could not fetch the remote version (likely a network issue). Please try again later."
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$LOCAL_VER" = "$REMOTE_VER" ]; then
|
||
t "✅ 已是最新版(${LOCAL_VER}),不需更新。" \
|
||
"✅ Already up to date (${LOCAL_VER}), nothing to update."
|
||
t " (仍會同步模板邏輯檔,確保 hooks/commands 與最新一致。)" \
|
||
" (Template logic files will still be synced to keep hooks/commands in line with the latest.)"
|
||
echo ""
|
||
fi
|
||
|
||
# ── 工具函式 ───────────────────────────────────────
|
||
# 覆蓋更新:模板/邏輯檔,無條件抓最新版蓋掉。
|
||
update_file() {
|
||
local dest="$1" src="$2"
|
||
mkdir -p "$(dirname "$dest")"
|
||
if [ -f "$dest" ]; then
|
||
if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && ! looks_like_error_page "$dest.tmp"; then
|
||
if cmp -s "$dest" "$dest.tmp"; then
|
||
rm -f "$dest.tmp" # 內容相同,不算更新
|
||
else
|
||
mv "$dest.tmp" "$dest"
|
||
UPDATED+=("$dest")
|
||
fi
|
||
else
|
||
rm -f "$dest.tmp"
|
||
FAILED+=("$dest")
|
||
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
|
||
fi
|
||
else
|
||
if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then
|
||
NEW+=("$dest") # 新功能:舊版沒有的檔
|
||
else
|
||
rm -f "$dest"
|
||
FAILED+=("$dest")
|
||
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# 保留:使用者資料檔,只記錄「有保留」,永遠不動。
|
||
keep_file() {
|
||
[ -f "$1" ] && KEPT+=("$1") || true
|
||
}
|
||
|
||
# 客製檔:使用者一定會手填內容(如 pre-write-guard.sh)。
|
||
# - 已存在 → 絕不覆蓋,但把最新模板版抓到 <檔名>.template.sh 旁邊,供使用者自行 diff 採納。
|
||
# - 不存在 → 視同新檔,直接抓本體(第一次安裝才會走這條)。
|
||
keep_with_template() {
|
||
local dest="$1" src="$2"
|
||
if [ -f "$dest" ]; then
|
||
KEPT+=("$dest")
|
||
local tmpl="${dest%.sh}.template.sh"
|
||
if curl -sSL "$src" -o "$tmpl.tmp" 2>/dev/null && ! looks_like_error_page "$tmpl.tmp"; then
|
||
if [ -f "$tmpl" ] && cmp -s "$tmpl" "$tmpl.tmp"; then
|
||
rm -f "$tmpl.tmp" # 模板版沒變,不重複提示
|
||
else
|
||
mv "$tmpl.tmp" "$tmpl"
|
||
TEMPLATED+=("$tmpl")
|
||
fi
|
||
else
|
||
rm -f "$tmpl.tmp"
|
||
fi
|
||
else
|
||
update_file "$dest" "$src" # 還沒裝過 → 當新檔處理
|
||
fi
|
||
}
|
||
|
||
# ── 偵測已安裝哪些模組(依現有檔案判斷,更新只動已裝的)──
|
||
HAS_WIKI=false
|
||
HAS_SDD=false
|
||
[ -d ".claude/wiki" ] && HAS_WIKI=true
|
||
if [ -f ".claude/hooks/sdd-guard.sh" ] || [ -d "docs/3-specs/TEMPLATE-sdd" ]; then HAS_SDD=true; fi
|
||
|
||
t "📦 偵測到已安裝模組:" "📦 Detected installed modules:"
|
||
$HAS_WIKI && echo " • LLM Wiki"
|
||
$HAS_SDD && echo " • SDD"
|
||
{ $HAS_WIKI || $HAS_SDD; } || \
|
||
t " (未偵測到任何模組——這裡可能還沒安裝,請改跑 install.sh)" \
|
||
" (No modules detected — nothing installed here yet; run install.sh instead.)"
|
||
echo ""
|
||
|
||
# ── 客製檔:使用者手填的 guardrail,永不覆蓋(issue #3)──
|
||
# pre-write-guard.sh 的定位是「空白客製模板,使用者沒配置前不提供保護」(CHANGELOG 1.2.0)。
|
||
# 下游通常已塞滿自己的 enforcement,直接覆蓋=無聲關掉整套 guardrail。
|
||
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
|
||
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
|
||
|
||
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
|
||
# 共用 hook 與指引
|
||
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
|
||
update_file ".claude/VERSION" "$TEMPLATE_URL/.claude/VERSION"
|
||
|
||
if $HAS_WIKI; then
|
||
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋
|
||
update_file ".claude/wiki/INDEX.md" "$TEMPLATE_URL/.claude/wiki/INDEX.md"
|
||
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
|
||
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
|
||
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
|
||
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
|
||
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
|
||
update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md"
|
||
# Cowork(claude.ai)的 wiki 整理 skill:規則檔,可覆蓋
|
||
update_file "docs/SKILL.md" "$TEMPLATE_URL/docs/SKILL.md"
|
||
|
||
# wiki 的「使用者資料」:絕不碰
|
||
keep_file ".claude/wiki/status.md"
|
||
keep_file ".claude/wiki/mistakes.md"
|
||
keep_file ".claude/wiki/decisions-summary.md"
|
||
keep_file ".claude/wiki/TAXONOMY.md"
|
||
keep_file ".claude/wiki/.wikiignore"
|
||
fi
|
||
|
||
if $HAS_SDD; then
|
||
# SDD 範本與 hook:可覆蓋
|
||
update_file "docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/docs/3-specs/TEMPLATE-sdd/design.md"
|
||
update_file "docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/docs/3-specs/TEMPLATE-sdd/tasks.md"
|
||
update_file "docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/docs/2-architecture/decisions/TEMPLATE-adr.md"
|
||
update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md"
|
||
update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh"
|
||
fi
|
||
|
||
# ── 自我更新:把最新的 update.sh 也抓下來(含 install.sh)──
|
||
# 這兩支在 main/scripts/ 下,不在 template/。
|
||
update_file "scripts/update.sh" "$REPO_RAW/scripts/update.sh"
|
||
update_file "scripts/install.sh" "$REPO_RAW/scripts/install.sh"
|
||
|
||
chmod +x .claude/hooks/*.sh scripts/*.sh 2>/dev/null || true
|
||
|
||
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
|
||
keep_file ".claude/settings.json"
|
||
keep_file "CLAUDE.md"
|
||
|
||
# ── 結果輸出 ───────────────────────────────────────
|
||
echo ""
|
||
echo "─────────────────────────────────"
|
||
if [ ${#NEW[@]} -gt 0 ]; then
|
||
echo ""
|
||
t "🆕 新功能(舊版沒有,已加入):" "🆕 New features (absent in the old version, now added):"
|
||
for f in "${NEW[@]}"; do echo " + $f"; done
|
||
fi
|
||
if [ ${#UPDATED[@]} -gt 0 ]; then
|
||
echo ""
|
||
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
|
||
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
|
||
fi
|
||
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
|
||
echo ""
|
||
t "✨ 模板邏輯檔已全部最新,無需變動。" \
|
||
"✨ All template logic files are already up to date — no changes needed."
|
||
fi
|
||
if [ ${#KEPT[@]} -gt 0 ]; then
|
||
echo ""
|
||
t "🔒 完整保留(你的內容/設定,從未碰過):" \
|
||
"🔒 Fully preserved (your content/settings, never touched):"
|
||
for f in "${KEPT[@]}"; do echo " = $f"; done
|
||
fi
|
||
if [ ${#TEMPLATED[@]} -gt 0 ]; then
|
||
echo ""
|
||
t "📋 客製檔有新版範本(你的原檔沒動,新版另存旁邊,請自行 diff 採納):" \
|
||
"📋 Custom files have a new template version (your original is untouched; the new one is saved alongside — diff and adopt as you like):"
|
||
for f in "${TEMPLATED[@]}"; do
|
||
echo " → $f"
|
||
t " 比對:diff \"${f%.template.sh}.sh\" \"$f\"" \
|
||
" compare: diff \"${f%.template.sh}.sh\" \"$f\""
|
||
done
|
||
fi
|
||
|
||
# ── settings.json 提醒:新模組 hook 可能要手動補 ──
|
||
if [ -f ".claude/settings.json" ]; then
|
||
MISSING=()
|
||
$HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh")
|
||
$HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
|
||
$HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh")
|
||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||
echo ""
|
||
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
|
||
"📌 settings.json is yours (untouched), but these hooks are missing — please add them manually:"
|
||
for h in "${MISSING[@]}"; do echo " • $h"; done
|
||
fi
|
||
fi
|
||
|
||
# 有任何檔案抓取失敗要出聲,不能安靜吞掉(404 內容已被 looks_like_error_page 擋掉、
|
||
# 不會污染既有檔案,但使用者必須知道「這幾個檔沒更新到」)。
|
||
if [ ${#FAILED[@]} -gt 0 ]; then
|
||
echo ""
|
||
t "❌ 抓取失敗(來源不可達,原檔已保留不動):" "❌ Fetch failed (source unreachable, original files kept untouched):"
|
||
for f in "${FAILED[@]}"; do echo " x $f"; done
|
||
t " 請檢查網路,或用 TEMPLATE_SOURCE=<其他來源> 重跑。" \
|
||
" Check your network, or rerun with TEMPLATE_SOURCE=<alternate source>."
|
||
fi
|
||
|
||
echo ""
|
||
t "🚀 更新完成:${LOCAL_VER} → ${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER} → ${REMOTE_VER}"
|
||
t " 下次更新直接跑:bash scripts/update.sh" " Next time, just run: bash scripts/update.sh"
|
||
t " 改了什麼看:CHANGELOG.md" " See what changed: CHANGELOG.md"
|
||
echo ""
|
||
|
||
if [ ${#FAILED[@]} -gt 0 ]; then
|
||
exit 1
|
||
fi
|