6a49f25aef
SDD: docs/3-specs/jdd-dual-profile(draft → active,leo 2026-08-05 回「開工」)
範圍:總管指定的「防炸兩件 → Phase 0 → Phase 1」,Phase 2 以後未開工。
■ 防炸(排在所有 task 之前,因為它們炸的是既有的東西)
- check-no-instance-names.sh + instance-names.txt:框架範本不得混入實例專名
基線實測 22 行命中(非先前誤報的 20)→ 9 行無損泛化改寫、13 行檔級豁免記帳待 W3 搬走
拒絕假性清理(把專名換成模糊詞=資訊消失、分層問題還在)
- check-legacy-paths.sh:已發佈腳本引用的 35 條遠端路徑只增不移
舊實例跑的是舊腳本、路徑寫死;搬檔=整排 404 且不會有下一次更新來修它(1.16.0 前科)
■ Phase 0 地基
- template/manifest/{common,repo,orchestrator}.tsv:安裝清單單一真相源
修好 install/update 兩份硬編清單的既有漂移——install 從不裝 wiki-first-search /
subagent-wiki-guard / publish-lag-check / decisions-summary,但 update 會
⇒ 乾淨安裝反而拿不到 1.16/1.17/1.18 的招牌功能
- .claude/hooks/lib/role-lib.sh:scope×role 兩軸機械判定,零自陳
身分矩陣六組實測全通過,含「成員 repo × orchestrator」不存在的格子擋下
- .sdt-framework-dev:框架開發標記(官方沒有 --framework-dev 這個參數,實查非記憶)
■ Phase 1 雙 profile
- profiles/{repo,orchestrator}/CLAUDE.md 兩部憲法
- install.sh:--profile + 自動偵測+寫檔前確認、manifest 驅動、
CLAUDE.md 三段組裝(框架區/本地補充區界標+sha256)、.profile、.template-manifest、
settings.json 寫入 env.AGENT_ROLE 預設
- update.sh:漂移偵測(不覆蓋手改檔、另存 .new、白話清單)+ 基準快照隨更新前進
- template/CLAUDE.md 原路徑凍結留底(相容)
■ 順手修掉兩個舊 bug(都在本次要動的函式裡)
- add_if_missing 少了 mkdir -p ⇒ 新目錄的檔 curl 失敗但 VERSION 照升(2026-07 記「待回報」至今未修)
- 下載健全性只用 [ -s ]=非空即接受 ⇒ 404 頁面會無聲覆寫好檔
(SKILL.md 260→1 行的機制;同一支腳本的版本號那條路早就防了,檔案這條沒防)
■ 實測(非推論)
- G4 憲法分流:兩個乾淨環境各裝一次,orchestrator 版含 SDD 三件式關鍵字 0 次、
repo 版含上游指針 8 次;界標 4/4;sha 宣告與實算相符
- G7 CI 擋實例名:注入違規行 → fail 並指出 sdd-check.md:77,exit 1;還原後 exit 0
- 漂移偵測:手改兩支 hook → 正確報 2 支、手改內容保住、產 .new;
解掉後歸零;連跑三輪冪等
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
528 lines
27 KiB
Bash
Executable File
528 lines
27 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:「要發佈的正稿,從頭就不要用奇怪的網址,
|
||
# 以免改來改去」)。發佈時不再需要改寫網址——寫死的就是對外正確的那一個。
|
||
# 內部要改指私有草稿源(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|<html'; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ── 工具函式 ───────────────────────────────────────
|
||
# 覆蓋更新:模板/邏輯檔,抓最新版蓋掉——**除非它被手改過**。
|
||
update_file() {
|
||
local dest="$1" src="$2"
|
||
mkdir -p "$(dirname "$dest")"
|
||
MANAGED+=("$dest\toverwrite")
|
||
if [ -f "$dest" ]; then
|
||
if is_drifted "$dest"; then
|
||
# 被手改過:不覆蓋,把新版放旁邊供 diff
|
||
if curl -sSL "$src" -o "$dest.new" 2>/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 <file>.new <file>"
|
||
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 ""
|