#!/bin/bash # system-dev-template installer # 已有專案接入腳本——只建立缺少的東西,已有的一律不動。 # # 模組化安裝: # --wiki 只裝 LLM Wiki(記憶系統 + 機敏防護) # --sdd 只裝 SDD 系統(動 code 前必須有 design.md) # --all 兩個都裝(預設) # 無參數 互動式詢問 # # 為什麼留在同一個 repo 用參數選,而不是 fork: # 使用者多半非專業,最怕「我要去哪個 repo」。一個入口 + 選單最友善。 # 等未來功能多到 3+ 個再演進成「模板組合器」。模組邊界先在這裡劃好。 set -euo pipefail # ── i18n:依 locale 選語言,預設英文 ────────────────── # 為什麼預設英文:curl | bash 常是 LANG=C,外國人預設就該看得懂; # 台灣使用者 locale 多為 zh_TW,會自動切回繁中。 case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in zh*|*Hant*|*Hans*) IS_ZH="yes" ;; *) IS_ZH="no" ;; esac # t "中文" "English" → 依語系印出對應字串 t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; } # tn = 不換行版(給 prompt 用) 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/install.sh TEMPLATE_SOURCE="${TEMPLATE_SOURCE:-https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main}" REPO_URL="$TEMPLATE_SOURCE/template" CREATED=() SKIPPED=() 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| 安裝時偵測到的來源型態:**${source_kind}** > CC 與 Cowork 整理/讀取「人寫的原始文件」時,**只在這裡找、只在這裡動**。 | 項目 | 值 | |------|----| | 來源型態 | \`${source_kind}\` | | raw source | \`${RAW_SOURCE}\` | **約束(CC 與 Cowork 都必須遵守)** - 整理 wiki/知識時,原始文件**一律從上方 raw source 路徑讀取**,不要假設是 \`docs/\`。 BLOCK if [ "$IS_VAULT" = "yes" ]; then cat < Source type detected at install time: **${source_kind}** > When CC and Cowork curate/read human-written raw source, **look only here and act only here**. | Item | Value | |------|-------| | Source type | \`${source_kind}\` | | raw source | \`${RAW_SOURCE}\` | **Constraints (both CC and Cowork must obey)** - When curating the wiki/knowledge, **always read raw source from the path above** — don't assume \`docs/\`. BLOCK if [ "$IS_VAULT" = "yes" ]; then cat </dev/null && ! looks_like_error_page "$dest"; then CREATED+=("$dest") else rm -f "$dest" FAILED+=("$dest $(tn "(來源抓不到:$src)" "(source unreachable: $src)")") fi else SKIPPED+=("$dest $(tn '(已存在,跳過)' '(already exists, skipped)')") fi } # ── 共用結構(兩個模組都需要 docs 分類 + .claude)── create_dir "docs/1-vision" create_dir "docs/2-architecture/decisions" create_dir "docs/4-guides" create_dir "docs/5-records/incidents" create_dir "docs/5-records/test-reports" create_dir "docs/6-user" create_dir ".claude/commands" create_dir ".claude/hooks" download_if_missing "docs/README.md" "$REPO_URL/docs/README.md" # ── WIKI 模組 ───────────────────────────────────── if $WANT_WIKI; then create_dir ".claude/wiki" download_if_missing ".claude/wiki/INDEX.md" "$REPO_URL/.claude/wiki/INDEX.md" download_if_missing ".claude/wiki/TAXONOMY.md" "$REPO_URL/.claude/wiki/TAXONOMY.md" download_if_missing ".claude/wiki/status.md" "$REPO_URL/.claude/wiki/status.md" download_if_missing ".claude/wiki/mistakes.md" "$REPO_URL/.claude/wiki/mistakes.md" download_if_missing ".claude/wiki/decisions-summary.md" "$REPO_URL/.claude/wiki/decisions-summary.md" download_if_missing ".claude/wiki/.wikiignore" "$REPO_URL/.claude/wiki/.wikiignore" download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md" download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md" download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md" download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md" # wiki 相關 hooks:接關 + 機敏掃描 download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh" download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh" # Cowork(claude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則 # (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。 download_if_missing "docs/SKILL.md" "$REPO_URL/docs/SKILL.md" fi # ── SDD 模組 ────────────────────────────────────── if $WANT_SDD; then create_dir "docs/3-specs" download_if_missing "docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/docs/3-specs/TEMPLATE-sdd/design.md" download_if_missing "docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/docs/3-specs/TEMPLATE-sdd/tasks.md" download_if_missing "docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/docs/2-architecture/decisions/TEMPLATE-adr.md" download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md" download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh" fi # ── 共用 hook:專案自訂禁令骨架(預設停用)──────── download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh" # ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)── download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md" chmod +x .claude/hooks/*.sh 2>/dev/null || true # ── 依模組產生 settings.json 的 hooks 區塊 ──────── # settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。 build_hooks_json() { local session_hooks="" pretool_hooks="" if $WANT_WIKI; then session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }' fi # PreToolUse 依模組疊加 local pt=() $WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }') pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }') $WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }') local IFS=, pretool_hooks="${pt[*]}" printf '{\n "hooks": {\n' if [ -n "$session_hooks" ]; then printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks" fi printf ' "PreToolUse": [\n { "matcher": "Write|Edit",\n "hooks": [ %s ] }\n ]\n' "$pretool_hooks" printf ' }\n}\n' } if [ ! -f ".claude/settings.json" ]; then build_hooks_json > .claude/settings.json CREATED+=(".claude/settings.json $(tn "(依 $MODULE 模組產生)" "(generated for module: $MODULE)")") else SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')") fi # ── CLAUDE.md:只在完全不存在時建立 ──────────────── # 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入, # 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。 if [ ! -f "CLAUDE.md" ]; then download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md" if [ -f "CLAUDE.md" ]; then emit_raw_source_block >> CLAUDE.md CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE})" "← raw source declaration written (${VAULT_TYPE})")") fi else SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')") fi # ── 輸出結果 ────────────────────────────────────── echo "" t "✅ 建立了:" "✅ Created:" # 注意:macOS bash 3.2 在 set -u 下展開「空陣列」會炸 unbound variable, # 所以這裡先確認有元素才展開(SKIPPED 區塊在下方本來就有守,CREATED 補上)。 if [ ${#CREATED[@]} -gt 0 ]; then for item in "${CREATED[@]}"; do echo " + $item"; done fi if [ ${#SKIPPED[@]} -gt 0 ]; then echo "" t "⚠️ 跳過(已存在):" "⚠️ Skipped (already exists):" for item in "${SKIPPED[@]}"; do echo " - $item"; done fi # 來源抓不到要出聲,不能安靜吞掉——404 內容不會被寫進檔案(已在 download_if_missing 擋掉), # 但使用者必須知道「這幾個檔沒裝到」,不然會誤以為裝完整了。 if [ ${#FAILED[@]} -gt 0 ]; then echo "" t "❌ 抓取失敗(來源不可達,未寫入任何檔案):" "❌ Fetch failed (source unreachable, no file was written):" for item in "${FAILED[@]}"; do echo " x $item"; done t " 請檢查網路,或用 TEMPLATE_SOURCE=<其他來源> 重跑。" \ " Check your network, or rerun with TEMPLATE_SOURCE=." fi echo "" echo "─────────────────────────────────" # CLAUDE.md 已存在 → 依模組提醒手動加區塊 if [ -f "CLAUDE.md" ]; then if ! grep -q "raw source" CLAUDE.md; then echo "" t "📌 CLAUDE.md 已存在但缺少 raw source 宣告。" \ "📌 CLAUDE.md exists but lacks a raw source declaration." t " 請手動把以下區塊貼進去,讓 CC 與 Cowork 知道原始文件在哪、不要亂動既有結構:" \ " Paste the block below in so CC and Cowork know where the raw source is and won't disturb your structure:" emit_raw_source_block | sed 's/^/ /' fi if $WANT_WIKI && ! grep -q "wiki/status.md" CLAUDE.md; then echo "" t "📌 CLAUDE.md 已存在但缺少 wiki 讀取順序,請手動加入:" \ "📌 CLAUDE.md exists but lacks the wiki reading order — please add it manually:" echo "" if [ "$IS_ZH" = "yes" ]; then cat <<'SNIP' ## Wiki 讀取順序 | 檔案 | 時機 | 用途 | |------|------|------| | `.claude/wiki/status.md` | session 開始第一件事 | 當前進度 | | `.claude/wiki/mistakes.md` | 做新功能前 | 已知誤解 | | `.claude/wiki/decisions-summary.md` | 設計判斷時 | 架構決策 | SNIP else cat <<'SNIP' ## Wiki reading order | File | When | Purpose | |------|------|---------| | `.claude/wiki/status.md` | first thing at session start | current progress | | `.claude/wiki/mistakes.md` | before building a new feature | known misconceptions | | `.claude/wiki/decisions-summary.md` | when making design calls | architecture decisions | SNIP fi fi if $WANT_SDD && ! grep -q "docs/3-specs" CLAUDE.md; then echo "" t "📌 CLAUDE.md 已存在但缺少 SDD 鐵律,請手動加入:" \ "📌 CLAUDE.md exists but lacks the SDD iron rule — please add it manually:" echo "" if [ "$IS_ZH" = "yes" ]; then cat <<'SNIP' ## 絕對鐵律 1. 任何 code 變動前必須有對應 SDD(docs/3-specs/[子系統]/design.md) 找不到 → 停手問負責人,不要自行建立。 SNIP else cat <<'SNIP' ## Iron rule 1. Every code change must have a matching SDD (docs/3-specs/[subsystem]/design.md). Not found → stop and ask the owner; do not create one on your own. SNIP fi fi fi # settings.json 已存在 → 依模組提醒要合併哪些 hook if [ -f ".claude/settings.json" ]; then MISSING_HOOKS=() $WANT_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING_HOOKS+=("SessionStart: session-start-recall.sh") $WANT_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): wiki-secret-scan.sh") $WANT_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): sdd-guard.sh") if [ ${#MISSING_HOOKS[@]} -gt 0 ]; then echo "" t "📌 .claude/settings.json 已存在,請手動把以下 hooks 合併進去(保留既有設定):" \ "📌 .claude/settings.json exists — merge the hooks below in manually (keep your existing settings):" for h in "${MISSING_HOOKS[@]}"; do echo " • $h"; done fi fi # pre-write-guard 是空殼,提醒它預設不攔(避免「以為有保護其實沒有」的安全錯覺) echo "" t "ℹ️ .claude/hooks/pre-write-guard.sh 是「按需手填的空插槽」,預設不攔任何東西。" \ "ℹ️ .claude/hooks/pre-write-guard.sh is an empty slot to fill on demand — by default it blocks nothing." t " 需要專案禁令?最簡單是叫你的 CC 寫一支貼合的 guard hook(比範本表達力強);" \ " Need project-specific bans? Easiest is to ask your CC to write a tailored guard hook (more expressive than the template);" t " 或自己填 FORBIDDEN_PATTERNS 並到 settings.json 掛上才會生效。" \ " or fill in FORBIDDEN_PATTERNS yourself and wire it into settings.json to take effect." echo "" t "🚀 下一步:" "🚀 Next steps:" if $WANT_WIKI; then t " 在 Claude Code 對話裡執行 /wiki-init" \ " In a Claude Code conversation, run /wiki-init" t " CC 會掃描現有文件、套用 .wikiignore、建立 wiki。" \ " CC will scan your existing docs, apply .wikiignore, and build the wiki." fi if $WANT_SDD; then t " 動 code 前先在 docs/3-specs/[子系統]/ 建 design.md(可用 /sdd-check 協助)" \ " Before touching code, create design.md under docs/3-specs/[subsystem]/ (use /sdd-check to help)" fi t " GitHub issue:CC 可直接 /issue-handle 讀回自己 repo 的 issue(禁自動輪詢)" \ " GitHub issues: CC can use /issue-handle to read issues from its own repo (no auto-polling)" echo "" # 有任何來源抓取失敗 → 用非零 exit code 出聲,不能靜靜地當「裝完了」收工。 if [ ${#FAILED[@]} -gt 0 ]; then exit 1 fi