2f5d9f3bb2
SDD: docs/3-specs/jdd-dual-profile(active)。編號 task 26/33 完成,Phase 4-5 未開工。 ■ Phase 2 JDD 文件範本(orchestrator profile) 範本形狀對齊「實際跑出來的那兩份」(總管已寫的 root.md 15 卡、journeys.md J-1 九站), 不是照規格憑空造: - 卡片是巢狀 bullet(`- **P1** 🟢 …` + 子項放來源/對帳),非規格畫的平行文字行 - 站點索引**巢狀 bullet 不用表格**(表格會把層級壓平,看不出從屬) - 兩份都保留「這卷還缺什麼(誠實記)」收尾段——規格沒有,但那是防假綠的地方 新增:root.md / journeys.md / sprint.md / triage-map.md 四範本(add-if-missing, 填了就永不覆蓋)+ plugin-load-order.md(W3 插槽,框架不發明平行外掛格式) ■ Phase 3 封路 hook(八條規則落六支檔) - role-guard(J1+J2+J3)★命門:考生不能改考卷。六組實測含「考題藏在別的 md 裡」也擋 - jdd-format-guard(J4+J5+J8):紅卡缺對帳日/任務缺站號/PM 文件混技術名詞 - station-done-guard(J6):收工判準是站的考題全綠,不是任務全關 - regression-scope(J7):動實作 → 列出要重考哪幾題(只提醒不擋) - install-artifact-guard(S1):實例不改機制 - orchestrator-scope-guard(S4):總管不進成員 repo 動實作(從實例上收進框架, 路徑清單改由實例自填,範本零專名) 掛載鏈依「範圍大的擋在前」:改機制 → 角色 → 位置 → 格式 → 既有三支 ■ 還清兩件舊債 - update.sh 檔案清單改讀 manifest(舊硬編降為抓不到來源時的 fallback) ——install/update 兩份手抄清單漂移的根因全修 - CLAUDE.md 界標補植:舊實例全文原封包進本地區、框架區重鋪、原檔備份、冪等 ——解開「沒界標⇒不敢覆蓋⇒框架改的憲法永遠送不到既有實例」這個死結 ■ 修掉三個自己造的問題(實測抓出來的,不是想出來的) - jdd-format-guard 誤擋真實 journeys.md 的「這卷還缺什麼」自述段 → 排除法改**正面圈定**(只掃卡片本體與站內文),說明區/自述段/索引自然不在範圍 - install-artifact-guard 把 pre-write-guard.sh 也擋了——而它的錯誤訊息正叫人去改那支 → 使用者自訂插槽列為最優先放行 - check-legacy-paths 用 HEAD 當基準會**自我弱化**:改成清單驅動後保護範圍 35→29 條 → 基準改指最後一次真正發佈的版本 ■ 實測(全部貼過輸出) - G2 考生改考卷:6/6,含 orchestrator 寫 code/engineer 改考題/考題藏別處 - G4 憲法分流:兩環境重裝,總管版技術軌關鍵字 0、成員版上游指針 8,界標 4/4 - G6 實例改機制:4/4,含框架開發標記放行與自訂插槽放行 - G7 CI 擋實例名:注入違規 → 指出檔案行號 exit 1 - G3 進度以站計量:起牀推「J-1 已點亮 2/9 站」、收工列未亮站並禁用任務數當理由 - 回歸考、界標補植冪等、orchestrator-scope-guard 四組:全通 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
649 lines
32 KiB
Bash
Executable File
649 lines
32 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"
|
||
|
||
# ── 舊版硬編清單(僅在抓不到 manifest 時使用的退路)──────────
|
||
# 保留它的唯一理由:來源不可達時,更新不該整個停擺。
|
||
# ⚠️ 新增產物請改 template/manifest/*.tsv,**不要**再往這裡加——
|
||
# 這份清單就是當初漂移的來源。
|
||
legacy_update_list() {
|
||
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
|
||
# 共用 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
|
||
|
||
}
|
||
|
||
# ── Manifest 驅動的更新(1.19.0:兩支腳本讀同一份清單)────────
|
||
# 在這之前,這裡是一份**手抄的**檔案清單,install.sh 另有一份。兩份手抄必然漂移,
|
||
# 而且已經漂了:wiki-first-search / subagent-wiki-guard / publish-lag-check /
|
||
# decisions-summary 這四個,update 會處理但 install 從來不裝
|
||
# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。
|
||
# 現在兩支都讀 template/manifest/*.tsv,這類漂移在結構上不可能再發生。
|
||
#
|
||
# 讀不到 manifest(來源太舊/網路問題)→ 退回舊的硬編清單(見下方 legacy_update_list),
|
||
# 不讓更新整個停擺。
|
||
|
||
MANIFEST_DIR="system-dev/.template-manifest.d"
|
||
mkdir -p "$MANIFEST_DIR"
|
||
|
||
SDT_PROFILE="repo"
|
||
[ -f "system-dev/.profile" ] && SDT_PROFILE="$(tr -d '[:space:]' < system-dev/.profile 2>/dev/null || echo repo)"
|
||
|
||
fetch_manifest() { # $1=名稱
|
||
local out="$MANIFEST_DIR/$1.tsv"
|
||
if curl -sSL "$TEMPLATE_URL/manifest/$1.tsv" -o "$out" 2>/dev/null \
|
||
&& [ -s "$out" ] && ! looks_like_error_page "$out"; then
|
||
return 0
|
||
fi
|
||
rm -f "$out"; return 1
|
||
}
|
||
|
||
resolve_src() {
|
||
case "$1" in
|
||
T:*) printf '%s/%s' "$TEMPLATE_URL" "${1#T:}" ;;
|
||
S:*) printf '%s/%s' "$REPO_RAW/scripts" "${1#S:}" ;;
|
||
*) printf '' ;;
|
||
esac
|
||
}
|
||
|
||
want_module() {
|
||
case "$1" in
|
||
core) return 0 ;;
|
||
wiki) $HAS_WIKI && return 0 || return 1 ;;
|
||
sdd) $HAS_SDD && return 0 || return 1 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
apply_manifest() { # $1=manifest 檔
|
||
local f="$1" src dest class module profile url
|
||
[ -f "$f" ] || return 0
|
||
while IFS=$'\t' read -r src dest class module profile; do
|
||
case "$src" in ''|'#'*) continue ;; esac
|
||
[ -z "${dest:-}" ] && continue
|
||
want_module "$module" || continue
|
||
url="$(resolve_src "$src")"
|
||
case "$class" in
|
||
dir) mkdir -p "$dest" 2>/dev/null || true ;;
|
||
overwrite) [ -n "$url" ] && update_file "$dest" "$url" ;;
|
||
add-if-missing) [ -n "$url" ] && add_if_missing "$dest" "$url" ;;
|
||
keep-with-template) [ -n "$url" ] && keep_with_template "$dest" "$url" ;;
|
||
keep) keep_file "$dest" ;;
|
||
claude-md) CLAUDE_MD_SRC="$url" ;; # 憲法走界標補植,見下方專段
|
||
*) : ;;
|
||
esac
|
||
done < "$f"
|
||
}
|
||
|
||
CLAUDE_MD_SRC=""
|
||
USED_MANIFEST=false
|
||
if fetch_manifest common; then
|
||
USED_MANIFEST=true
|
||
fetch_manifest "$SDT_PROFILE" || true
|
||
apply_manifest "$MANIFEST_DIR/common.tsv"
|
||
apply_manifest "$MANIFEST_DIR/$SDT_PROFILE.tsv"
|
||
else
|
||
t "⚠️ 抓不到安裝清單,退回舊版硬編清單(功能較少但不會壞)" \
|
||
"⚠️ Manifest unavailable; falling back to the legacy hardcoded list"
|
||
legacy_update_list
|
||
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"
|
||
|
||
# ── CLAUDE.md 界標補植(1.19.0)────────────────────────
|
||
# 1.18.x 以前的 CLAUDE.md 是「整份下載 + 往後 append」,**沒有任何區段界標**。
|
||
# 沒界標 ⇒ update 分不出「哪段是框架的、哪段是你寫的」⇒ 只能永遠不敢碰
|
||
# ⇒ 框架後來改的憲法**永遠送不到既有實例**。這一段就是來解這個死結。
|
||
#
|
||
# 保守到底,寧可什麼都不做也不弄丟一個字:
|
||
# · 已有界標 → 什麼都不做(冪等)
|
||
# · 沒有界標 → 現有全文**原封不動**包進本地補充區,框架區重鋪在它上面
|
||
# · 抓不到憲法範本 → 放棄補植、保留原檔(不留半殘的檔)
|
||
# 最壞情況是同一段內容在兩區各一份,由人自己刪——我們不代刪。
|
||
if [ -f "CLAUDE.md" ]; then
|
||
if grep -q 'sdt:framework begin' CLAUDE.md 2>/dev/null; then
|
||
keep_file "CLAUDE.md"
|
||
elif [ -n "${CLAUDE_MD_SRC:-}" ]; then
|
||
FW_TMP="$(mktemp)"
|
||
if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && ! looks_like_error_page "$FW_TMP"; then
|
||
cp CLAUDE.md "CLAUDE.md.before-markers"
|
||
{
|
||
printf '<!-- sdt:framework begin profile=%s version=%s sha256=%s -->\n' \
|
||
"$SDT_PROFILE" "$REMOTE_VER" "$(sdt_sha256 "$FW_TMP" | cut -c1-12)"
|
||
cat "$FW_TMP"
|
||
printf '<!-- sdt:framework end -->\n\n'
|
||
printf '<!-- sdt:local begin — 這一區是你的,update 永遠不會動它 -->\n'
|
||
printf '\n<!-- ⬇️ 以下是你原本的 CLAUDE.md,一字未動地搬進來。\n'
|
||
printf ' 上面的框架區從現在起由 template 維護;兩邊若有重複的段落,\n'
|
||
printf ' 請自己刪掉這裡的舊版本(我們不敢代你刪)。\n'
|
||
printf ' 原檔備份:CLAUDE.md.before-markers -->\n\n'
|
||
cat "CLAUDE.md.before-markers"
|
||
printf '\n<!-- sdt:local end -->\n'
|
||
} > CLAUDE.md
|
||
rm -f "$FW_TMP"
|
||
MIGRATED+=("CLAUDE.md → 補上界標(原檔備份 CLAUDE.md.before-markers)")
|
||
else
|
||
rm -f "$FW_TMP"
|
||
keep_file "CLAUDE.md"
|
||
fi
|
||
else
|
||
keep_file "CLAUDE.md"
|
||
fi
|
||
fi
|
||
|
||
# ── 結果輸出 ───────────────────────────────────────
|
||
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 ""
|