#!/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:「要發佈的正稿,從頭就不要用奇怪的網址, # 以免改來改去」)。發佈時不再需要改寫網址——寫死的就是對外正確的那一個。 # 內部要改指私有草稿源(Gitea/未來 CF git)時,用環境變數覆寫,不改檔: # 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=() 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 # ── 漂移偵測(1.19.0 新增)───────────────────────── # 病根:實例會手改安裝產物(hook、範本),而**沒有任何機制知道**。 # 實測基線(2026-08-05,拿一個真實使用中的實例對照框架): # 6 支框架 hook 裡 **4 支已被手改**,另有 11 支是該實例自己發明的。 # 兩層後果:① 框架修好的 bug 送不到那些檔 ② 手改內容沒人審、沒回饋回框架 # ⇒ 同一個坑每個實例各踩一次。 # # 判準:安裝當下的 sha(記在 system-dev/.template-manifest)vs 現在的 sha。 # 相同 → 沒動過 → 安全覆蓋成新版 # 不同 → **被手改過** → 不覆蓋、另存 .new、列進漂移清單,讓人自己決定 # # 為什麼不直接覆蓋:無聲吃掉別人的修改(他多半是在修一個框架還沒修的問題)。 # 為什麼不永遠不覆蓋:那更新就永遠送不到 = 等於沒有更新機制。 # **分辨得出來**才是解法,兩個極端都不對。 DRIFTED=() sdt_sha256() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else printf 'nohash'; fi } MANIFEST_FILE="system-dev/.template-manifest" manifest_sha() { # 查登記的 sha;查不到回空 [ -f "$MANIFEST_FILE" ] || return 0 awk -F'\t' -v d="$1" '!/^#/ && $1==d {print $4; exit}' "$MANIFEST_FILE" } # 本次 update 經手過的檔(不論更新、保留或漂移)。收工時用它重寫 manifest。 # ⚠️ 沒有這一步的話會出現**自己造的 bug**:update 把檔覆蓋成新版後,manifest 還留著舊 sha # ⇒ 下一次跑 update 會把「框架自己更新的檔」誤判成「使用者手改」,整排假漂移。 # 基準快照必須跟著實檔一起前進。 MANAGED=() is_drifted() { # 0=被手改過;1=沒動過或無從判斷(保守,不誤報) local dest="$1" recorded recorded="$(manifest_sha "$dest")" [ -z "$recorded" ] && return 1 [ -f "$dest" ] || return 1 [ "$(sdt_sha256 "$dest")" = "$recorded" ] && return 1 return 0 } # ── 下載健全性檢查(修一個舊 bug)────────────────── # 原本只用 [ -s ]=「非空就接受」。但 curl 對 404 會把「404: Not Found」當內容寫出來, # **那也是非空** ⇒ 好檔被覆寫成一行垃圾,而且沒有任何錯誤訊息。 # 實錄:SKILL.md 曾被無聲覆寫 260 行 → 1 行(template issue #13)。 # 諷刺的是本腳本的「版本號比對」那段早就防了這招(見上方 REMOTE_VER 的驗證), # 檔案下載這條路卻沒防——同一個坑,防了一半。 looks_like_error_page() { local f="$1" [ -s "$f" ] || return 0 # 空的 → 當失敗 # 極短又長得像錯誤訊息 → 當失敗(正常範本檔不會只有一兩行還寫著 404) if [ "$(wc -l < "$f" | tr -d ' ')" -le 2 ] && head -c 200 "$f" | grep -qiE '40[0-9]|not found|/dev/null && ! looks_like_error_page "$dest.new"; then if cmp -s "$dest" "$dest.new"; then rm -f "$dest.new" # 手改後剛好等於新版 → 沒事,不用吵 else DRIFTED+=("$dest") fi else rm -f "$dest.new" DRIFTED+=("$dest") fi return 0 fi 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" 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" 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" MANAGED+=("$dest\tadd-if-missing") if [ -f "$dest" ]; then KEPT+=("$dest") return 0 fi # 🐛 修:原本少了 mkdir -p ⇒ 目標在**新目錄**時 curl 直接失敗, # 但 VERSION 照樣升上去 = 典型假綠(2026-07 在一個實例升級時撞到,當時記「待回報 template」)。 mkdir -p "$(dirname "$dest")" if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then NEW+=("$dest") else rm -f "$dest" t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $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" # Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project # 投影共用同一套;任一模組在用就補/更新(邏輯檔,可覆蓋)。 if $HAS_WIKI || $HAS_SDD; then update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md" fi 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" # 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG) update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh" update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh" # 1.18.0:公開 mirror 落後偵測(手動同步的靜默失敗只有外部用戶會撞到) update_file ".claude/hooks/publish-lag-check.sh" "$TEMPLATE_URL/.claude/hooks/publish-lag-check.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" # vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):邏輯檔,可覆蓋。舊版沒有 → 當新檔補。 update_file ".claude/commands/wiki-extract.md" "$TEMPLATE_URL/.claude/commands/wiki-extract.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" # SDD 生命週期鐵律(1.14,issue #6):規則檔+獨立檢查腳本=邏輯檔可覆蓋; # pending-changes.md 裝著用戶的 proposal=用戶資料,只補不覆蓋。 # (issue #13 教訓:update 不補新檔會造成結構斷層,新檔必須在這裡鋪。) update_file "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$TEMPLATE_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md" add_if_missing "system-dev/docs/3-specs/pending-changes.md" "$TEMPLATE_URL/system-dev/docs/3-specs/pending-changes.md" update_file "system-dev/scripts/sdd-active-check.sh" "$TEMPLATE_URL/scripts/sdd-active-check.sh" # tasks⇄Project 投影(issue #16):邏輯檔,可覆蓋。舊版沒有 → add_if_missing 補。 # 啟用狀態存遠端(acr push),不在這些檔裡,覆蓋不會關掉誰的同步。 add_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.yaml" add_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.local.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 system-dev/workflows/*.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 [ ${#DRIFTED[@]} -gt 0 ]; then echo "" t "⚠️ 下列 ${#DRIFTED[@]} 個檔被手改過,這一版**沒有**覆蓋它們:" \ "⚠️ ${#DRIFTED[@]} file(s) were modified by hand and were NOT overwritten:" for f in "${DRIFTED[@]}"; do if [ -f "$f.new" ]; then t " ✋ $f → 新版已放在 $f.new,請 diff" \ " ✋ $f → new version saved as $f.new — please diff" else t " ✋ $f (新版抓取失敗,稍後再試)" \ " ✋ $f (couldn't fetch the new version; try again later)" fi done echo "" t " 這代表什麼:你(或某個 CC)改過框架發下來的檔。" \ " What this means: you (or a CC) edited a file that the framework ships." t " 兩條路,擇一——別放著不管,放著=框架之後的修正永遠送不到這個檔:" \ " Pick one — leaving it means future framework fixes will never reach this file:" t " ① 把你的改動寫成框架提案(推薦)——一次修全家,別人也拿得到" \ " 1. Turn your change into a framework proposal (recommended) — fixes it for everyone" t " ② 放棄本地改動:mv <檔>.new <檔>" \ " 2. Drop your local change: mv .new " 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") # ── 1.16.0:兩支 wiki 讀取 hook 自動註冊(不只提醒)── # 理由:這兩支的整個存在意義就是「不依賴任何人記得」。 # 若靠人看提醒去手動補 settings.json,等於把同一個病搬到安裝環節。 if $HAS_WIKI && command -v python3 >/dev/null 2>&1; then python3 - <<'PYEOF' 2>/dev/null || true import json, os p = ".claude/settings.json" try: with open(p) as f: d = json.load(f) except Exception: raise SystemExit(0) # 壞掉的 settings 不碰,交給下方提醒 pre = d.setdefault("hooks", {}).setdefault("PreToolUse", []) blob = json.dumps(pre) added = [] # 1.16.1:既有註冊若漏 Bash(原版只掛 Grep|Glob|Read)就地補上—— # 破口實例:用 curl/wrangler 亂試部署方法走 Bash,整支 hook 不觸發。 for _e in pre: if "wiki-first-search" in json.dumps(_e) and "Bash" not in _e.get("matcher", ""): _e["matcher"] = "Grep|Glob|Read|Bash"; added.append("wiki-first-search(補Bash)") if "wiki-first-search" not in blob: pre.append({"matcher": "Grep|Glob|Read|Bash", "hooks": [ {"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"}]}) added.append("wiki-first-search") if "subagent-wiki-guard" not in blob: pre.append({"matcher": "Task", "hooks": [ {"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"}]}) added.append("subagent-wiki-guard") if added: with open(p, "w") as f: json.dump(d, f, ensure_ascii=False, indent=2) print(" ✅ 已自動註冊 wiki 讀取 hook:" + "、".join(added)) PYEOF fi 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 # ── 重寫漂移基準快照(manifest)────────────────────── # 兩個作用: # ① 舊實例(1.18.x 以前沒有 manifest)→ 這一輪之後就有了,下次起就能偵測漂移。 # 注意本輪它們**不會**被判漂移(查無登記=不判定),這是刻意的保守: # 沒有基準就宣稱「你改過」等於瞎猜。第一輪建基準,第二輪起才有話語權。 # ② 已有 manifest → 把「本輪更新過的檔」的 sha 推進到新值。 # 少了這步就會自己造一個 bug:框架自己覆蓋的檔,下一輪被誤判成使用者手改。 # 漂移中的檔**不更新其基準**——它們仍然是「相對於安裝版被改過」,下次還要繼續報。 { printf '# dest\tclass\tversion\tsha256 —— update 維護的漂移基準,勿手改\n' for entry in ${MANAGED[@]+"${MANAGED[@]}"}; do m_dest="$(printf '%b' "$entry" | cut -f1)" m_class="$(printf '%b' "$entry" | cut -f2)" [ -f "$m_dest" ] || continue skip="" for d in ${DRIFTED[@]+"${DRIFTED[@]}"}; do [ "$d" = "$m_dest" ] && skip="yes" && break done if [ -n "$skip" ]; then # 保留原登記值(它還在漂移中,基準不能跟著漂) old="$(manifest_sha "$m_dest")" [ -n "$old" ] && printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$LOCAL_VER" "$old" continue fi printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$REMOTE_VER" "$(sdt_sha256 "$m_dest")" done } > "$MANIFEST_FILE.tmp" && mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE" 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 ""