ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份

leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
              「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
                你自己可以 dogfooding」

搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。

51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。

尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 11:41:46 +08:00
commit c2638668e3
85 changed files with 11879 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
# deploy-web.sh — install/landing 的唯一部署入口(2026-07-28 立)
#
# 為什麼存在:leo 問「以後會出現改了沒推的問題嗎?」誠實答案=文字規範會被忘,
# 只有機制可靠。本腳本把「驗語法→部署→驗版本有變→抓線上→記錄」焊成一步——
# 用它部署,就不可能出現 t79 那種「commit 了但部署失敗沒人發現」。
#
# 用法:scripts/deploy-web.sh installer|landing
# 部署狀態記錄在 system-dev/docs/4-guides/deploy-state.json(誰、何時、版本、檔案 sha)
# ⇒ 「改了沒推」隨時可查:比對該檔現在的 sha 與記錄裡的 sha。
set -euo pipefail
cd "$(dirname "$0")/.."
TOP="$(pwd)"
STATE="$TOP/system-dev/docs/4-guides/deploy-state.json"
TARGET="${1:-}"
# 部署源可用 DEPLOY_SRC 覆蓋(07-30:舊寫法寫死 scratchpad 且靜默 fallback
# scratchpad 是 session 專屬目錄,被清掉後部署源就斷——事故傳導路徑,故必印出本次部署源)
SCRATCH="${DEPLOY_SRC:-/private/tmp/claude-501/-Users-youlinhsieh-Documents-tech-projects-InkStoneCo/92a75156-c295-4c79-bfeb-de1a20e4ed26/scratchpad}"
case "$TARGET" in
installer)
DIR="$SCRATCH/rag-installer/installer/oauth-prototype"
if [ ! -d "$DIR" ]; then
echo "⚠️ 部署源 $DIR 不存在(scratchpad 被清?),改用 git 工作樹那份"
DIR="$TOP/products/arcrun-rag/installer/oauth-prototype"
[ -d "$DIR" ] || { echo "❌ 備援部署源也不存在(products 的 checkout 不在含 oauth-prototype 的分支);用 DEPLOY_SRC= 指定部署源"; exit 1; }
fi
FILE="$DIR/worker.js"; CFG="--config ./wrangler.toml"; URL="https://install.arcrun.dev/" ;;
landing)
DIR="$TOP/products/arcrun-rag/landing"
FILE="$DIR/worker.js"; CFG=""; URL="https://rag.arcrun.dev/" ;;
*) echo "用法:$0 installer|landing" >&2; exit 2 ;;
esac
echo "▶ 本次部署源=$DIR"
echo "① esbuild 驗語法(node --check 抓不到 template literal 內的錯,t79 教訓)"
# --external:cloudflare:* Workers runtime 內建模組(如 cloudflare:email),
# esbuild 不認識它們但 wrangler 認得;不標 external 會誤判成語法錯(07-29 landing 踩到)。
( cd "$DIR" && npx esbuild worker.js --bundle --format=esm --outfile=/dev/null \
--external:cloudflare:* --external:./migrations.json --external:./workflows.json 2>/dev/null \
|| npx esbuild worker.js --bundle --format=esm --outfile=/dev/null --external:cloudflare:* )
echo "①a 釘死 URL 必須真的存在(07-29 事故:釘碼用短碼拼湊出不存在的 commit,安裝全 404"
if [ "$TARGET" = "installer" ]; then
BASE=$(grep -o "https://cdn.jsdelivr.net/gh/[^']*" "$FILE" | head -1)
CODE=$(curl -s -m 30 -o /dev/null -w '%{http_code}' "$BASE/manifest.json")
[ "$CODE" = "200" ] || { echo "❌ BUNDLE_BASE 指向的 manifest 回 $CODE(釘碼錯或 bundle 沒推):$BASE"; exit 1; }
echo " $BASE/manifest.json → 200"
fi
echo "①b 文案契約測試(防「改好的又改錯」——禁句出現=拒絕部署)"
if [ -f "$DIR/copy-contract.test.mjs" ]; then ( cd "$DIR" && node copy-contract.test.mjs ); fi
echo "①c 防回退閘(07-30:版本號變動必須是人明示的決定,不能是部署源掉包的副作用)"
LIVE_VER="$(curl -s -m 25 "${URL}?cb=$RANDOM" | grep -o '20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]+[0-9a-f]\{7\}' | head -1 || true)"
if [ "$TARGET" = "installer" ]; then
NEXT_BUILT="$(grep -o "BUNDLE_BUILT = '[^']*'" "$FILE" | head -1 | sed "s/.*'\(.*\)'/\1/" || true)"
NEXT_PIN="$(grep -o 'arcrun-rag-bundles@[0-9a-f]*' "$FILE" | head -1 | cut -d@ -f2 | cut -c1-7 || true)"
{ [ -n "$NEXT_BUILT" ] && [ -n "$NEXT_PIN" ]; } && NEXT_VER="${NEXT_BUILT}+${NEXT_PIN}" || NEXT_VER=""
else
NEXT_VER="$(grep -o 'SITE_BUNDLE_VERSION = "[^"]*"' "$DIR/wrangler.toml" 2>/dev/null | sed 's/.*"\(.*\)"/\1/' || true)"
fi
if [ -n "$LIVE_VER" ] && [ -n "$NEXT_VER" ] && [ "$LIVE_VER" != "$NEXT_VER" ] && [ "${ALLOW_VERSION_CHANGE:-}" != "1" ]; then
echo "❌ 版本號會變:線上 $LIVE_VER → 要部署 $NEXT_VER"
echo " 換版是人的決定:確定要換,重跑時帶 ALLOW_VERSION_CHANGE=1"
exit 1
fi
echo " 線上 ${LIVE_VER:-(抓不到)} / 待部 ${NEXT_VER:-(推導不出)}$([ "${ALLOW_VERSION_CHANGE:-}" = "1" ] && echo '(已明示換版)')"
echo "①d 部署源必須落在 git07-30:t143-t150 十筆只在本機沒推、雲端誤判程式碼失蹤的教訓)"
PENDING_GIT=false
if git -C "$DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
DIRTY="$(git -C "$DIR" status --porcelain -- "$(basename "$FILE")" | head -1 || true)"
UNPUSHED="$(git -C "$DIR" log --oneline '@{u}..HEAD' 2>/dev/null | wc -l | tr -d ' ' || true)"
[ -n "$DIRTY" ] && { echo " ⚠️⚠️ $(basename "$FILE") 有未 commit 修改——部署的東西 git 裡沒有!"; PENDING_GIT=true; }
if [ "$UNPUSHED" = "" ] || ! git -C "$DIR" rev-parse '@{u}' >/dev/null 2>&1; then
echo " ⚠️⚠️ 所在分支沒設 upstream——commit 了也沒人推得到 Gitea"; PENDING_GIT=true
elif [ "$UNPUSHED" != "0" ]; then
echo " ⚠️⚠️ 有 $UNPUSHED 筆 commit 未推上 Giteagit log @{u}..HEAD"; PENDING_GIT=true
fi
$PENDING_GIT || echo " 部署源乾淨且已同步 Gitea ✓"
else
echo " ⚠️⚠️ 部署源不在任何 git 工作樹裡——這份程式碼沒有版控!"; PENDING_GIT=true
fi
# 封測期不擋出貨,但留痕:pending_git=true =「線上跑的和 Gitea 不一致」查得到
STATE_PATH="$STATE" python3 - "$TARGET" "$PENDING_GIT" <<'PY'
import json,sys,os
p=os.environ.get('STATE_PATH'); t,pg=sys.argv[1],sys.argv[2]=='true'
try: d=json.load(open(p))
except Exception: d={}
d.setdefault(t,{})['pending_git']=pg
json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)
PY
echo "② 部署(uncle6"
set -a; source "$TOP/.env" 2>/dev/null || true; set +a
OUT="$(cd "$DIR" && CLOUDFLARE_ACCOUNT_ID=58309bb90fd93ad6d0fe0aae99170e9d npx wrangler deploy $CFG 2>&1)"
VID="$(printf '%s' "$OUT" | grep -o 'Current Version ID: [a-f0-9-]*' | awk '{print $4}')"
[ -n "$VID" ] || { echo "❌ 部署失敗(沒有 Version ID):"; printf '%s\n' "$OUT" | tail -8; exit 1; }
echo " Version ID: $VID"
echo "③ 版本必須有變(防『部署了但還是舊版』)"
PREV="$(python3 -c "
import json,sys
try: print(json.load(open('$STATE')).get('$TARGET',{}).get('version',''))
except Exception: print('')" 2>/dev/null)"
if [ "$VID" = "$PREV" ] && [ -n "$PREV" ]; then
echo "❌ Version ID 與上次相同($VID)=內容沒變或部署被跳過"; exit 1
fi
echo "④ 線上實測(帶 cache-buster"
CODE="$(curl -s -m 25 -o /tmp/deploy_check.html -w '%{http_code}' "${URL}?cb=$RANDOM")"
[ "$CODE" = "200" ] || { echo "❌ 線上回 $CODE"; exit 1; }
echo " $URL → 200$(wc -c </tmp/deploy_check.html | tr -d ' ') B"
echo "⑤ 記錄(供 drift 檢查:檔案 sha ≠ 記錄 sha = 改了沒推)"
SHA="$(shasum -a 256 "$FILE" | cut -d' ' -f1)"
STATE_PATH="$STATE" python3 - "$TARGET" "$VID" "$SHA" <<'PY'
import json,sys,datetime,os
p=os.environ.get('STATE_PATH')
t,v,sha=sys.argv[1],sys.argv[2],sys.argv[3]
try: d=json.load(open(p))
except Exception: d={}
d[t]={'version':v,'file_sha256':sha,'deployed_at':datetime.datetime.now().isoformat(timespec='seconds')}
json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)
print(f" {t}: {v} / sha {sha[:16]}")
PY
echo "✅ 完成。之後查「改了沒推」:shasum 該檔 vs $STATE"