#!/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; } REPO_RAW="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main" TEMPLATE_URL="$REPO_RAW/template" UPDATED=() KEPT=() NEW=() TEMPLATED=() MIGRATED=() COEXIST=() # ── 版本比對:先看本機 vs 遠端,給使用者「值不值得更新」的判斷 ── # VERSION 新位置在 system-dev/,舊位置在 .claude/(1.8.x 以前)。優先讀新、回退舊。 LOCAL_VER="$(tn '(未知)' '(unknown)')" if [ -f "system-dev/VERSION" ]; then LOCAL_VER="$(tr -d '[:space:]' < system-dev/VERSION)" elif [ -f ".claude/VERSION" ]; then LOCAL_VER="$(tr -d '[:space:]' < .claude/VERSION)" fi REMOTE_VER="$(curl -sSL "$TEMPLATE_URL/system-dev/VERSION" 2>/dev/null | tr -d '[:space:]' || echo '')" # 容錯:curl 對 404 會把「404:NotFound」當內容輸出(非空),舊版誤把它寫進 VERSION。 # 這裡驗證必須像版號(X.Y.Z),否則一律視為取不到,避免污染 VERSION 檔。 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 # ── 結構遷移(1.9.0):舊版把 wiki/VERSION 放 .claude/、工具 docs 放根 docs/ ── # 新版一律收進 system-dev/。這裡冪等遷移:偵測舊位置 → 搬到 system-dev/,已搬過則略過。 # 必須在「模組偵測」之前跑(偵測靠目錄存在與否判斷,搬完才看得到新位置)。 # # 安全原則: # - wiki 整包搬(含 cards/ 與可能的 wiki/.git),用 mv 保留內含 .git。 # - docs 只搬「工具自己鋪的白名單」子目錄;用戶自填在 docs/ 的其他內容一律不動。 # - 目的地已存在同名 → 不覆蓋(保留用戶在新位置的東西),略過該項。 migrate_dir() { # $1=舊路徑 $2=新路徑 local from="$1" to="$2" [ -e "$from" ] || return 0 # 舊的不存在 → 無需遷移 if [ -e "$to" ]; then # 目的地已存在。兩種可能: # (a) 已遷移過 → 舊位置不該還在;冪等略過即可。 # (b) 用戶先 install 建了空殼 → 舊位置仍有真資料,現在「並存」。 # 不能靜默跳過 (b),也絕不自動合併(覆蓋風險)。→ 記為「並存待合併」,警告。 COEXIST+=("$from ↔ $to") return 0 fi mkdir -p "$(dirname "$to")" if mv "$from" "$to" 2>/dev/null; then MIGRATED+=("$from → $to") fi } # 任一舊位置還在 → 需要遷移(遷移本身冪等:已搬的項目會被 migrate_dir 略過)。 NEEDS_MIGRATE="no" if [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ] \ || [ -d "docs/3-specs" ] || [ -f "docs/SKILL.md" ] || [ -f "docs/README.md" ]; then NEEDS_MIGRATE="yes" fi if [ "$NEEDS_MIGRATE" = "yes" ]; then t "🔧 偵測到舊版結構,遷移到 system-dev/ …" "🔧 Old layout detected — migrating into system-dev/ …" mkdir -p system-dev # wiki(含 cards/ 與內含的 .git)整包搬 migrate_dir ".claude/wiki" "system-dev/wiki" # 工具版號 migrate_dir ".claude/VERSION" "system-dev/VERSION" # 工具文件白名單(只搬工具鋪的,用戶自填的 docs 內容不動) migrate_dir "docs/SKILL.md" "system-dev/docs/SKILL.md" migrate_dir "docs/README.md" "system-dev/docs/README.md" migrate_dir "docs/1-vision" "system-dev/docs/1-vision" migrate_dir "docs/2-architecture" "system-dev/docs/2-architecture" migrate_dir "docs/3-specs" "system-dev/docs/3-specs" migrate_dir "docs/4-guides" "system-dev/docs/4-guides" migrate_dir "docs/5-records" "system-dev/docs/5-records" migrate_dir "docs/6-user" "system-dev/docs/6-user" 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 && [ -s "$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" t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest" fi else if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then NEW+=("$dest") # 新功能:舊版沒有的檔 else rm -f "$dest" t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest" fi fi } # 保留:使用者資料檔,只記錄「有保留」,永遠不動。 keep_file() { [ -f "$1" ] && KEPT+=("$1") || true } # 補新檔:舊版沒有、新版才有的「使用者資料檔」(如 principles.md)。 # 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。 add_if_missing() { local dest="$1" src="$2" if [ -f "$dest" ]; then KEPT+=("$dest") elif curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then NEW+=("$dest") else rm -f "$dest" fi } # 客製檔:使用者一定會手填內容(如 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 && [ -s "$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 } # ── 偵測已安裝哪些模組(依現有檔案判斷,更新只動已裝的)── # 遷移已在上面跑完,這裡看新位置 system-dev/。 HAS_WIKI=false HAS_SDD=false [ -d "system-dev/wiki" ] && HAS_WIKI=true if [ -f ".claude/hooks/sdd-guard.sh" ] || [ -d "system-dev/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 "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION" if $HAS_WIKI; then # wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/,hooks/commands 留 .claude/。 update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/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 "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md" # wiki 的「使用者資料」:絕不碰 keep_file "system-dev/wiki/status.md" keep_file "system-dev/wiki/mistakes.md" # principles.md(1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留 add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md" keep_file "system-dev/wiki/decisions-summary.md" keep_file "system-dev/wiki/TAXONOMY.md" keep_file "system-dev/wiki/.wikiignore" fi if $HAS_SDD; then # SDD 範本與 hook:可覆蓋 update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md" update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/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 抓到 system-dev/scripts/ ── # 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。 update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh" update_file "system-dev/scripts/install.sh" "$REPO_RAW/scripts/install.sh" chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh 2>/dev/null || true # ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」── keep_file ".claude/settings.json" keep_file "CLAUDE.md" # ── 結果輸出 ─────────────────────────────────────── echo "" echo "─────────────────────────────────" if [ ${#MIGRATED[@]} -gt 0 ]; then echo "" t "📦 結構遷移(已收進 system-dev/):" "📦 Layout migrated (moved into system-dev/):" for f in "${MIGRATED[@]}"; do echo " ⇒ $f"; done fi if [ ${#COEXIST[@]} -gt 0 ]; then echo "" t "🛑 偵測到 wiki 並存(新舊位置都有資料,需要合併):" \ "🛑 Coexisting wiki detected (both old and new locations have data — needs merging):" for f in "${COEXIST[@]}"; do echo " ↔ $f"; done t " 成因:先跑過 install(建了空殼)才遷移,舊位置真資料沒被搬。" \ " Cause: install ran first (created an empty shell), so migration skipped your real data in the old location." t " 不自動合併(避免覆蓋你的資料)。請叫你的 CC:" \ " Not auto-merged (to avoid overwriting your data). Ask your CC:" t " 「.claude/wiki/ 和 system-dev/wiki/ 並存,請逐檔比對、把真資料合進 system-dev/,再刪舊的」" \ " \"There are two wikis (.claude/wiki/ and system-dev/wiki/) — diff each file, merge the real data into system-dev/, then delete the old one.\"" fi 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 echo "" t "🚀 更新完成:${LOCAL_VER} → ${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER} → ${REMOTE_VER}" t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh" t " 改了什麼看:CHANGELOG.md" " See what changed: CHANGELOG.md" echo ""