@qijenchen/design-system 0.1.0-beta.69 → 0.1.0-beta.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ds-canonical/fork/governance.lock +112 -0
- package/ds-canonical/fork/hooks/block_prototype_imports.py +111 -0
- package/ds-canonical/fork/hooks/check_chrome_header_avatar_canonical.sh +95 -0
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +349 -0
- package/ds-canonical/fork/hooks/check_ds_anchor_preflight.sh +132 -0
- package/ds-canonical/fork/hooks/check_escape_marker_abuse.sh +140 -0
- package/ds-canonical/fork/hooks/check_field_family_invariants.sh +250 -0
- package/ds-canonical/fork/hooks/check_fork_product_quality.sh +36 -0
- package/ds-canonical/fork/hooks/check_item_list_gap.sh +54 -0
- package/ds-canonical/fork/hooks/check_layout_space_magic_numbers.sh +96 -0
- package/ds-canonical/fork/hooks/check_opacity_token_usage.sh +149 -0
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +196 -0
- package/ds-canonical/fork/hooks/check_sidebar_menu_button_implicit_wrap.sh +86 -0
- package/ds-canonical/fork/hooks/check_tailwind_wildcard_in_docs.sh +79 -0
- package/ds-canonical/fork/hooks/inject_deploy_url_after_push.sh +238 -0
- package/ds-canonical/fork/launchers/fork-governance-dispatcher.sh +44 -0
- package/ds-canonical/fork/launchers/inject_fork_governance_preamble.sh +46 -0
- package/ds-canonical/fork/launchers/settings-hooks.json +58 -0
- package/ds-canonical/fork/manifest.json +79 -0
- package/ds-canonical/fork/preamble.md +495 -0
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +19 -0
- package/ds-canonical/references/scenario-definition.md +4 -4
- package/ds-canonical/skills/design-system-audit/SKILL.md +1 -1
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +5 -1
- package/src/story-governance/category-matrix.json +132 -0
- package/src/tokens/utility-registry.json +124 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# inject_deploy_url_after_push.sh — UserPromptSubmit + PostToolUse: 偵測 git push 後自動 inject deploy URL
|
|
3
|
+
#
|
|
4
|
+
# Per user verbatim 2026-05-26:「完成部署之後都應該自動回吐部署的連結,每次必定自動回,不論是現在這個 session 還是其他的」
|
|
5
|
+
# Per user verbatim 2026-05-27:「不管在任何 repo,只要有部署東西到 netlify 上不管是否是 production 都應該要提供連結」
|
|
6
|
+
#
|
|
7
|
+
# Mechanism(2026-05-27 v2 expand scope per user complaint — DS GH Pages also auto-provide URL):
|
|
8
|
+
# PostToolUse Bash:tool_input.command 含 `git push origin <branch>` → 偵測 → multi-target URL detection:
|
|
9
|
+
# 1. Netlify(scripts/deploy-url.mjs + .netlify/state.json)— PW + fork
|
|
10
|
+
# 2. Netlify dashboard-link(netlify.toml exists,no state.json)— PW with Netlify auto-build
|
|
11
|
+
# 3. GitHub Pages(.github/workflows/*.yml 含 pages action)— DS repo
|
|
12
|
+
# → output URL list inject into AI context(下個 reply 必看到)
|
|
13
|
+
#
|
|
14
|
+
# 為何走 Hook(per CLAUDE.md governance 8-home L7 Hook 自動化):
|
|
15
|
+
# - 不靠 AI 記得「每次推完都要 echo URL」(會忘記 — 本 session user 抓「你他媽到底做得怎樣」)
|
|
16
|
+
# - 不靠 user 每次問「部署到哪?」(無聊重複)
|
|
17
|
+
# - Hook 機械保證每 push 必觸發,跨 session / 跨 fork user 自動受惠
|
|
18
|
+
#
|
|
19
|
+
# Scope expanded(2026-05-27):
|
|
20
|
+
# - Netlify CLI-linked (.netlify/state.json + scripts/deploy-url.mjs) → 直接 script 抓 URL
|
|
21
|
+
# - Netlify dashboard-linked (netlify.toml + branch deploys) → 用 git remote 推導 site name
|
|
22
|
+
# - GitHub Pages (.github/workflows/*.yml 含 pages.yml OR ci.yml deploy-pages) → 推導 GH Pages URL
|
|
23
|
+
#
|
|
24
|
+
# 對齊:.claude/skills/codex-collab/SKILL.md PostToolUse pattern + check_fork_user_plugin_install.sh detection pattern
|
|
25
|
+
|
|
26
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
27
|
+
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
30
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
31
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
32
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
33
|
+
|
|
34
|
+
# Scope:PostToolUse Bash 且 cmd 含 git push to remote(main / branch)
|
|
35
|
+
[ "$EVENT" != "PostToolUse" ] && exit 0
|
|
36
|
+
[ "$TOOL" != "Bash" ] && exit 0
|
|
37
|
+
|
|
38
|
+
# Heuristic:detect `git push origin <branch>` patterns
|
|
39
|
+
# 2026-06-06 fix:要求 `git push` 在「命令邊界」(行首 / ; / && / || / $( )擋字串提及(`P='git push origin'`)。
|
|
40
|
+
# 2026-06-07 fix(對抗 sweep):容忍 env-prefix(`GIT_SSH_COMMAND=… git push`)/ `git -C <dir>` /
|
|
41
|
+
# push 前後任意 flag(`--force-with-lease` / `-f` / `--set-upstream` / `--no-verify` 等)。原本只認 `-u`
|
|
42
|
+
# → 漏 fire 在這些「真實 push」上 = 該吐 URL 卻沒吐,defeat hook purpose。
|
|
43
|
+
_ENVP='([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*'
|
|
44
|
+
_GITPRE='git[[:space:]]+(-C[[:space:]]+[^[:space:]]+[[:space:]]+|-[A-Za-z][^[:space:]]*[[:space:]]+)*push'
|
|
45
|
+
_PUSHFLAGS='([[:space:]]+-{1,2}[A-Za-z][^[:space:]]*)*'
|
|
46
|
+
if ! echo "$CMD" | grep -qE "(^|[;&|(])[[:space:]]*${_ENVP}${_GITPRE}${_PUSHFLAGS}[[:space:]]+origin\b"; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Skip if push --delete (branch cleanup, not deploy)
|
|
51
|
+
if echo "$CMD" | grep -qE 'push\s+origin\s+--delete'; then
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
CWD=$(pwd)
|
|
56
|
+
# 2026-06-07 fix:偵測「真實 working dir」作 deploy-target 偵測根,跨 repo push 不吐「錯 repo」URL
|
|
57
|
+
# (anchor:syncing ds-product-template 時吐成 DS 站台 URL)。優先 `git -C <dir>`;否則命令鏈中
|
|
58
|
+
# 「最後一個 cd」(= push 時 effective cwd,**非第一個**;`echo x && cd /other && git push` 必用 /other)。
|
|
59
|
+
_GITC=$(echo "$CMD" | grep -oE 'git[[:space:]]+-C[[:space:]]+[^[:space:]]+' | head -1 | sed -E 's/.*-C[[:space:]]+//; s/^"//; s/"$//')
|
|
60
|
+
_LASTCD=$(echo "$CMD" | grep -oE '(^|[;&|(])[[:space:]]*cd[[:space:]]+[^;&|]+' | tail -1 | sed -E 's/.*cd[[:space:]]+//; s/[[:space:]]+$//; s/^"//; s/"$//')
|
|
61
|
+
for _d in "$_GITC" "$_LASTCD"; do
|
|
62
|
+
if [ -n "$_d" ] && [ -d "$_d" ] && git -C "$_d" rev-parse --git-dir >/dev/null 2>&1; then CWD="$_d"; break; fi
|
|
63
|
+
done
|
|
64
|
+
URLS_FOUND=""
|
|
65
|
+
# 2026-06-07 ROOT fix:只從「git push ... origin <ref>」這一段擷取 branch,不是整條 compound command 的
|
|
66
|
+
# 第一個 `origin X`。否則 `git fetch origin --quiet && git push origin main` 會誤抓 fetch 段的 `--quiet`
|
|
67
|
+
# 當 branch → 推導 `--quiet--site` 404(此前 5 道 guard 都沒擋到,因 `--quiet` 全是合法 git ref 字元)。
|
|
68
|
+
# 隔出 push 段(到下個 command 分隔 ; & | 為止)後,取 origin 後第一個 token。
|
|
69
|
+
# PUSH_SEG 用同一 env/-C/flag-tolerant prefix 隔出 push 段(到下個分隔 ; & | 為止),再取 origin 後第一 token
|
|
70
|
+
PUSH_SEG=$(echo "$CMD" | grep -oE "(^|[;&|(])[[:space:]]*${_ENVP}${_GITPRE}[^;&|]*" | head -1)
|
|
71
|
+
BRANCH=$(echo "$PUSH_SEG" | grep -oE 'origin[[:space:]]+[^[:space:]]+' | head -1 | awk '{print $2}')
|
|
72
|
+
# origin 後第一個 token 是 flag(`git push origin --force` = 無顯式 branch)→ 清空走 current-branch fallback
|
|
73
|
+
case "$BRANCH" in -*) BRANCH="" ;; esac
|
|
74
|
+
# 2026-06-06 fix:refspec `src:dst` → 取 dst(`HEAD:main` → `main`),否則推導出 `HEAD:main--site` 404 URL
|
|
75
|
+
case "$BRANCH" in *:*) BRANCH="${BRANCH##*:}" ;; esac
|
|
76
|
+
# 2026-06-07 fix:full-ref dst — `HEAD:refs/heads/main` → `main`(否則 `refs/heads/main--site` 誤判 preview);
|
|
77
|
+
# `refs/tags/...` → tag push,skip
|
|
78
|
+
case "$BRANCH" in refs/tags/*) exit 0 ;; esac
|
|
79
|
+
BRANCH="${BRANCH#refs/heads/}"
|
|
80
|
+
# 2026-06-06 fix:`git push origin HEAD`(或 `@`)= symbolic ref 指向當前 branch → 解析成真 branch 名。
|
|
81
|
+
# 2026-06-07 fix:detached HEAD 時 show-current 回「空字串 + exit 0」→ BRANCH 留空,交給下方 value-based fallback
|
|
82
|
+
# (原 `|| echo ""` 沒用,因 git 不是失敗而是成功回空)。
|
|
83
|
+
if [ "$BRANCH" = "HEAD" ] || [ "$BRANCH" = "@" ]; then
|
|
84
|
+
BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null)
|
|
85
|
+
fi
|
|
86
|
+
# 2026-06-06 fix:BRANCH 含非法 git ref 字元(" ' 空白 \ 等)→ 此命令只是「字串裡含 git push origin」
|
|
87
|
+
# (測試迴圈 / 文件 / echo),非真推送 → skip,避免把 `main"` 等垃圾推導成 404 URL 注入 context。
|
|
88
|
+
# git ref 合法字元集 ⊂ [A-Za-z0-9._/-];非此集 = 必為解析噪音(root guard,涵蓋 refspec/tag 之外的雜訊)。
|
|
89
|
+
if [ -n "$BRANCH" ] && ! echo "$BRANCH" | grep -qE '^[A-Za-z0-9._/-]+$'; then exit 0; fi
|
|
90
|
+
# 2026-06-06 fix:tag push(`v1.2.3` 等)不產 branch-preview / production deploy → skip,
|
|
91
|
+
# 否則把 tag 名當 branch 推導出 `v0.1.0-beta.56--site` 404 URL(release tag push 每次都誤吐)
|
|
92
|
+
if echo "$BRANCH" | grep -qE '^v[0-9]'; then exit 0; fi
|
|
93
|
+
if [ -n "$BRANCH" ] && git -C "$CWD" rev-parse --verify --quiet "refs/tags/$BRANCH" >/dev/null 2>&1; then exit 0; fi
|
|
94
|
+
# 2026-06-07 fix:仍空(bare `git push origin` / detached HEAD / 解析不出)→ 取 current branch(value-based);
|
|
95
|
+
# 仍空 → 無法產生合法 URL(會變 `https://--site` malformed)→ skip,不亂猜 main。
|
|
96
|
+
[ -z "$BRANCH" ] && BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null)
|
|
97
|
+
[ -z "$BRANCH" ] && exit 0
|
|
98
|
+
|
|
99
|
+
# v3 2026-05-27:curl HEAD verify URL before reporting(per user「你確定有做到」complaint)
|
|
100
|
+
# v4 2026-05-27:add content sniff(防 squat URLs 200 但 unrelated content)
|
|
101
|
+
verify_url() {
|
|
102
|
+
local url="$1"
|
|
103
|
+
local code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 5 -I "$url" 2>/dev/null)
|
|
104
|
+
case "$code" in
|
|
105
|
+
200|301|302) echo "OK" ;;
|
|
106
|
+
*) echo "FAIL:$code" ;;
|
|
107
|
+
esac
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# v4 content sniff:check 200 URL 是 Storybook real deploy(not squat)
|
|
111
|
+
# 用 Storybook hallmark patterns(sb-manager / sb-addons / @storybook/core title)
|
|
112
|
+
is_storybook_deploy() {
|
|
113
|
+
local url="$1"
|
|
114
|
+
curl -s --max-time 5 -L "$url" 2>/dev/null | grep -qE "sb-manager|sb-addons|@storybook/core|storybook-static"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Detection 1:Netlify CLI-linked(.netlify/state.json + scripts/deploy-url.mjs)
|
|
118
|
+
DEPLOY_SCRIPT="$CWD/scripts/deploy-url.mjs"
|
|
119
|
+
if [ -f "$DEPLOY_SCRIPT" ] && [ -f "$CWD/.netlify/state.json" ]; then
|
|
120
|
+
URL_INFO=$(node "$DEPLOY_SCRIPT" --json 2>/dev/null)
|
|
121
|
+
if [ -n "$URL_INFO" ]; then
|
|
122
|
+
URL=$(echo "$URL_INFO" | jq -r '.url // ""' 2>/dev/null)
|
|
123
|
+
IS_PROD=$(echo "$URL_INFO" | jq -r '.isProd // false' 2>/dev/null)
|
|
124
|
+
if [ -n "$URL" ]; then
|
|
125
|
+
if [ "$IS_PROD" = "true" ]; then
|
|
126
|
+
URLS_FOUND="${URLS_FOUND}🚀 Netlify PRODUCTION(${BRANCH}): ${URL}\n"
|
|
127
|
+
else
|
|
128
|
+
URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW(${BRANCH}): ${URL}\n"
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Detection 2:Netlify dashboard-linked(netlify.toml + no state.json)
|
|
135
|
+
# v4:try multiple naming conventions + content sniff to filter squat URLs
|
|
136
|
+
if [ -z "$URLS_FOUND" ] && [ -f "$CWD/netlify.toml" ]; then
|
|
137
|
+
GH_REMOTE=$(git -C "$CWD" remote get-url origin 2>/dev/null)
|
|
138
|
+
REPO_NAME=$(echo "$GH_REMOTE" | sed -E 's|.*/([^/.]+)(\.git)?$|\1|')
|
|
139
|
+
OWNER_REPO=$(echo "$GH_REMOTE" | sed -E 's|.*github\.com[:/]([^/]+/[^/.]+)(\.git)?$|\1|')
|
|
140
|
+
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
|
|
141
|
+
|
|
142
|
+
# v5 multi-candidate strategy(Netlify naming conventions in order of likelihood):
|
|
143
|
+
# 1. ~/.claude/local/deploy-targets.json overrides(per-user known URLs)— win all
|
|
144
|
+
# 2. <owner>-<package.json.name>.netlify.app(setup-netlify script convention,per scripts/setup-netlify-access.mjs `${ghUser}-${repoName}` formula)
|
|
145
|
+
# 3. <owner>-<repo-from-remote>.netlify.app(Netlify Import default,fork users without setup script)
|
|
146
|
+
# 4. <repo-from-remote>.netlify.app(simple,rare)
|
|
147
|
+
USER_OVERRIDE=""
|
|
148
|
+
if [ -f "$HOME/.claude/local/deploy-targets.json" ]; then
|
|
149
|
+
USER_OVERRIDE=$(jq -r --arg key "$OWNER_REPO" '.[$key] // ""' "$HOME/.claude/local/deploy-targets.json" 2>/dev/null)
|
|
150
|
+
fi
|
|
151
|
+
# Read package.json.name for setup-script convention
|
|
152
|
+
PKG_NAME=""
|
|
153
|
+
if [ -f "$CWD/package.json" ]; then
|
|
154
|
+
PKG_NAME=$(jq -r '.name // ""' "$CWD/package.json" 2>/dev/null | sed -E 's|^@[^/]+/||') # strip npm scope
|
|
155
|
+
fi
|
|
156
|
+
CANDIDATES=""
|
|
157
|
+
if [ -n "$USER_OVERRIDE" ]; then
|
|
158
|
+
CANDIDATES="$USER_OVERRIDE"
|
|
159
|
+
else
|
|
160
|
+
# Setup-script convention candidate(highest match rate for fork users following docs)
|
|
161
|
+
if [ -n "$OWNER" ] && [ -n "$PKG_NAME" ]; then CANDIDATES="$CANDIDATES https://${OWNER}-${PKG_NAME}.netlify.app"; fi
|
|
162
|
+
# Netlify Import default(no setup script, manual dashboard import)
|
|
163
|
+
if [ -n "$OWNER" ] && [ -n "$REPO_NAME" ] && [ "$REPO_NAME" != "$PKG_NAME" ]; then
|
|
164
|
+
CANDIDATES="$CANDIDATES https://${OWNER}-${REPO_NAME}.netlify.app"
|
|
165
|
+
fi
|
|
166
|
+
# Simple fallback
|
|
167
|
+
if [ -n "$REPO_NAME" ]; then CANDIDATES="$CANDIDATES https://${REPO_NAME}.netlify.app"; fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
REAL_URL=""; REAL_NOTE=""
|
|
171
|
+
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
172
|
+
# 2026-06-07 fix:優先「200 + Storybook content」;沒有則退「200(內容非 Storybook)」,
|
|
173
|
+
# **不丟棄**已驗 200 的 candidate。anchor:apps/template 是真 product app(非 Storybook)→ 原
|
|
174
|
+
# is_storybook_deploy 把活著的 200 deploy 誤當 squat 報「全 404」,違反 user「不管是否 production 都給連結」。
|
|
175
|
+
FALLBACK_URL=""
|
|
176
|
+
for candidate in $CANDIDATES; do
|
|
177
|
+
if [ "$(verify_url "$candidate")" = "OK" ]; then
|
|
178
|
+
if is_storybook_deploy "$candidate"; then
|
|
179
|
+
REAL_URL="$candidate"; REAL_NOTE="✅ verified 200 + Storybook content"; break
|
|
180
|
+
elif [ -z "$FALLBACK_URL" ]; then
|
|
181
|
+
FALLBACK_URL="$candidate"
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
done
|
|
185
|
+
if [ -z "$REAL_URL" ] && [ -n "$FALLBACK_URL" ]; then
|
|
186
|
+
REAL_URL="$FALLBACK_URL"; REAL_NOTE="✅ verified 200(內容非 Storybook — 可能 product app deploy)"
|
|
187
|
+
fi
|
|
188
|
+
if [ -n "$REAL_URL" ]; then
|
|
189
|
+
URLS_FOUND="${URLS_FOUND}🚀 Netlify PRODUCTION(${BRANCH}): ${REAL_URL} ${REAL_NOTE}\n"
|
|
190
|
+
else
|
|
191
|
+
URLS_FOUND="${URLS_FOUND}🚀 Netlify PRODUCTION URL 未驗 — tried: ${CANDIDATES// /, }\n ⚠️ 全 404。需要 user 手動 share dashboard URL,OR 創 \$HOME/.claude/local/deploy-targets.json:\n {\"${OWNER_REPO}\": \"https://<actual-site>.netlify.app\"}\n"
|
|
192
|
+
fi
|
|
193
|
+
else
|
|
194
|
+
# Branch preview:always use `<branch>--<sitename>` pattern,but sitename unknown unless USER_OVERRIDE
|
|
195
|
+
if [ -n "$USER_OVERRIDE" ]; then
|
|
196
|
+
SITENAME=$(echo "$USER_OVERRIDE" | sed -E 's|https?://([^.]+)\.netlify\.app.*|\1|')
|
|
197
|
+
CANDIDATE="https://${BRANCH}--${SITENAME}.netlify.app"
|
|
198
|
+
if [ "$(verify_url "$CANDIDATE")" = "OK" ]; then
|
|
199
|
+
URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW(${BRANCH}): ${CANDIDATE} ✅ verified 200\n"
|
|
200
|
+
else
|
|
201
|
+
URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW 推導: ${CANDIDATE} ⚠️ 404(preview 未啟 OR build pending — Netlify build 2-3 min)\n"
|
|
202
|
+
fi
|
|
203
|
+
else
|
|
204
|
+
URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW(${BRANCH}) — sitename 未知;設 \$HOME/.claude/local/deploy-targets.json 後 hook 可推 preview URL\n"
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Detection 3:GitHub Pages(.github/workflows/*.yml deploys to gh-pages OR uses actions/deploy-pages)
|
|
210
|
+
if ls "$CWD/.github/workflows/"*.yml >/dev/null 2>&1; then
|
|
211
|
+
if grep -l "actions/deploy-pages\|gh-pages\|github.io" "$CWD/.github/workflows/"*.yml >/dev/null 2>&1; then
|
|
212
|
+
GH_REMOTE=$(git -C "$CWD" remote get-url origin 2>/dev/null)
|
|
213
|
+
OWNER_REPO=$(echo "$GH_REMOTE" | sed -E 's|.*github\.com[:/]([^/]+/[^/.]+)(\.git)?$|\1|')
|
|
214
|
+
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
|
|
215
|
+
REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)
|
|
216
|
+
if [ -n "$OWNER" ] && [ -n "$REPO" ] && { [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; }; then
|
|
217
|
+
CANDIDATE="https://${OWNER}.github.io/${REPO}/"
|
|
218
|
+
VERIFY=$(verify_url "$CANDIDATE")
|
|
219
|
+
if [ "$VERIFY" = "OK" ]; then
|
|
220
|
+
URLS_FOUND="${URLS_FOUND}📄 GitHub Pages(${BRANCH}): ${CANDIDATE} ✅ verified 200\n"
|
|
221
|
+
else
|
|
222
|
+
URLS_FOUND="${URLS_FOUND}📄 GitHub Pages 推導 URL: ${CANDIDATE} ⚠️ ${VERIFY}(build ~3-5 min;若仍 404 check Actions tab build status)\n"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
# No deploy target detected → silent skip
|
|
229
|
+
[ -z "$URLS_FOUND" ] && exit 0
|
|
230
|
+
|
|
231
|
+
# Inject into AI context
|
|
232
|
+
# 2026-05-29 ROOT-CAUSE FIX:PostToolUse hook 的純 stdout **不會**注入 AI context(只進 transcript)→
|
|
233
|
+
# 原 `printf` 輸出讓 AI 看不到 URL → AI 每次 push 都沒 relay 給 user(user verbatim「部署完都沒給我 url」)。
|
|
234
|
+
# 必須輸出 JSON `hookSpecificOutput.additionalContext` 才會真注入 AI context。
|
|
235
|
+
MSG=$(printf '%b' "🚀 Deploy URLs auto-detected — RELAY 給 user(per user 2026-05-26「完成部署都自動回吐連結」+ 2026-05-27「不管 repo」):\n${URLS_FOUND}\n(AI:必須把上面 URL 貼給 user,不可省略)")
|
|
236
|
+
jq -n --arg ctx "$MSG" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$ctx}}'
|
|
237
|
+
|
|
238
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# fork-governance-dispatcher.sh — C-prime 治理派發器(committed in fork,極穩定、幾乎永不改)
|
|
3
|
+
#
|
|
4
|
+
# 這是 fork repo 唯一 commit 的「啟動器」:讀 npm 套件 ship 的 fork 治理 manifest,跑當下這個
|
|
5
|
+
# event 的所有官方 fork hook。DS 未來治理增刪改 → 重生 npm 套件的 manifest → fork `npm install`
|
|
6
|
+
# 後本派發器自動跑新的那套(committed 設定不用動)= 完全同步。
|
|
7
|
+
#
|
|
8
|
+
# 設計鐵則:
|
|
9
|
+
# - 本體在 node_modules(npm install 覆蓋 = 官方控管、使用者改不動)
|
|
10
|
+
# - manifest 缺(還沒 npm install)→ exit 0 永不 brick(bootstrap 另外發 notice)
|
|
11
|
+
# - 任一官方 fork hook exit 2(BLOCKER)→ 派發器 exit 2(轉發攔截)
|
|
12
|
+
# - 其餘 exit code 不阻擋(soft)
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
16
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
17
|
+
|
|
18
|
+
DS_FORK="${CLAUDE_PROJECT_DIR:-.}/node_modules/@qijenchen/design-system/ds-canonical/fork"
|
|
19
|
+
MANIFEST="$DS_FORK/manifest.json"
|
|
20
|
+
|
|
21
|
+
# 治理本體還沒就位(fork 第一個 session 還沒 npm install)→ 不阻擋(bootstrap 會發 notice)
|
|
22
|
+
[ -f "$MANIFEST" ] || exit 0
|
|
23
|
+
|
|
24
|
+
# 取這個 event 註冊的官方 fork hook 清單
|
|
25
|
+
HOOK_FILES=$(jq -r --arg ev "$EVENT" '.hooks[$ev][]?.file // empty' "$MANIFEST" 2>/dev/null)
|
|
26
|
+
[ -z "$HOOK_FILES" ] && exit 0
|
|
27
|
+
|
|
28
|
+
RC=0
|
|
29
|
+
while IFS= read -r hf; do
|
|
30
|
+
[ -z "$hf" ] && continue
|
|
31
|
+
body="$DS_FORK/$hf"
|
|
32
|
+
[ -f "$body" ] || continue # 個別 body 缺 → skip,不阻擋(派發器穩健)
|
|
33
|
+
# 依副檔名選 interpreter:.py 用 python3、其餘 bash。
|
|
34
|
+
# 鐵則:用 bash 跑 .py → syntax error → exit 2 → 被下面誤判成 BLOCKER → 擋掉所有編輯(致命)。
|
|
35
|
+
# python3 缺(罕見)→ exit 127(非 2)→ 不轉發 = fail-open,不 brick。
|
|
36
|
+
case "$body" in
|
|
37
|
+
*.py) echo "$INPUT" | python3 "$body" ;;
|
|
38
|
+
*) echo "$INPUT" | bash "$body" ;;
|
|
39
|
+
esac
|
|
40
|
+
hc=$?
|
|
41
|
+
[ "$hc" -eq 2 ] && RC=2 # 官方 hook 攔截 → 轉發攔截
|
|
42
|
+
done <<< "$HOOK_FILES"
|
|
43
|
+
|
|
44
|
+
exit $RC
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# inject_fork_governance_preamble.sh — C-prime SessionStart 治理 init(install-if-missing + 事前指引注入)
|
|
3
|
+
#
|
|
4
|
+
# 2026-06-17 合併原 check_governance_bootstrap 的 install(adversarial run-4 MAJOR — cloud-flow race):
|
|
5
|
+
# 原本 SessionStart 有 3 個 hook(bootstrap 裝 / inject 注入 / dispatcher),Claude Code 同組 hook「並行」跑。
|
|
6
|
+
# 雲端 fresh-clone 時 bootstrap 還在前景 `npm install`(冷裝 30s–4min),inject 並行跑完一次 `[ -f preamble ]`
|
|
7
|
+
# 檢查 → 檔案還沒到 → 靜默 exit 0 → 什麼都沒注入 = proactive 指引在雲端首次 session 失效(且每 session 重演)。
|
|
8
|
+
# 修:把 install 與 inject 放「同一 hook process 內 sequential」→ 消除並行 race + 避免兩個並發 npm install 衝突。
|
|
9
|
+
# dispatcher 不再掛 SessionStart(manifest 無 SessionStart body = 本來就 no-op);它留在 Pre/Post/UserPromptSubmit。
|
|
10
|
+
#
|
|
11
|
+
# fail-open:install 失敗 / preamble 缺 → notice + exit 0,永不 brick。
|
|
12
|
+
# 對齊既有用 additionalContext 的 hook 輸出契約。
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
PD="${CLAUDE_PROJECT_DIR:-.}"
|
|
16
|
+
FORK="$PD/node_modules/@qijenchen/design-system/ds-canonical/fork"
|
|
17
|
+
PREAMBLE="$FORK/preamble.md"
|
|
18
|
+
|
|
19
|
+
# (1) 治理本體缺(雲端 fresh-clone / 還沒裝)+ 有 package.json → 前景裝 @beta。
|
|
20
|
+
# 這裡 block 到裝完才往下讀 → sequential,後面的讀取保證在 install 之後(消除 race)。
|
|
21
|
+
# codex risk 2:plain `npm install` 會被 lockfile 重現舊樹 → 明確 @beta 拿最新治理。
|
|
22
|
+
if [ ! -f "$PREAMBLE" ] && [ -f "$PD/package.json" ]; then
|
|
23
|
+
echo "💡 DS 治理本體缺(可能雲端 fresh-clone session)→ 自動裝最新治理(@beta)…" >&2
|
|
24
|
+
( cd "$PD" && npm install @qijenchen/design-system@beta @qijenchen/storybook-config@beta --legacy-peer-deps --no-audit --no-fund >/dev/null 2>&1 ) || true
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# (2) 本體在(本機持久 / 剛裝完)→ 讀 + 經 additionalContext 注入設計紀律
|
|
28
|
+
if [ -f "$PREAMBLE" ]; then
|
|
29
|
+
CONTENT=$(cat "$PREAMBLE" 2>/dev/null)
|
|
30
|
+
if [ -n "$CONTENT" ]; then
|
|
31
|
+
jq -n --arg ctx "$CONTENT" '{
|
|
32
|
+
hookSpecificOutput: {
|
|
33
|
+
hookEventName: "SessionStart",
|
|
34
|
+
additionalContext: ("# DS 設計治理(npm-current,事前主動遵循)\n\n" + $ctx)
|
|
35
|
+
}
|
|
36
|
+
}'
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# (3) 仍缺(install 失敗 / 無 package.json)→ notice,不阻擋(fail-open)
|
|
42
|
+
cat >&2 << 'EOF'
|
|
43
|
+
💡 DS 治理尚未就位(npm install 未完成或失敗)。請手動執行 → npm install(或 npm run sync-all)。
|
|
44
|
+
注:現在「不阻擋」你工作,只是提醒;治理本體就位後設計治理 hook 才會機械把關。
|
|
45
|
+
EOF
|
|
46
|
+
exit 0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_generated": "build-fork-governance.mjs",
|
|
3
|
+
"_source": "template/ds-product-template/.claude/settings.json(hooks + permissions 區塊)",
|
|
4
|
+
"_merge": "sync-all/refresh-fork-launchers.mjs:strip 舊 launcher 註冊 + append canonical hooks + union permissions.allow",
|
|
5
|
+
"hooks": {
|
|
6
|
+
"SessionStart": [
|
|
7
|
+
{
|
|
8
|
+
"hooks": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject_fork_governance_preamble.sh\""
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PreToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/fork-governance-dispatcher.sh\""
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"PostToolUse": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "Edit|Write|MultiEdit|Bash",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/fork-governance-dispatcher.sh\""
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"UserPromptSubmit": [
|
|
39
|
+
{
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/fork-governance-dispatcher.sh\""
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"permissions": {
|
|
50
|
+
"allow": [
|
|
51
|
+
"Bash(npm install)",
|
|
52
|
+
"Bash(npm ci)",
|
|
53
|
+
"Bash(npm run *)",
|
|
54
|
+
"Bash(npx *)",
|
|
55
|
+
"Bash(git *)"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_generated": "build-fork-governance.mjs",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PostToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"file": "hooks/check_consumer_app_invariants.sh",
|
|
7
|
+
"sourceHook": "check_consumer_app_invariants.sh",
|
|
8
|
+
"bucket": "SHIP_AS_IS"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"file": "hooks/block_prototype_imports.py",
|
|
12
|
+
"sourceHook": "block_prototype_imports.py",
|
|
13
|
+
"bucket": "SHIP_AS_IS"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"file": "hooks/check_layout_space_magic_numbers.sh",
|
|
17
|
+
"sourceHook": "check_layout_space_magic_numbers.sh",
|
|
18
|
+
"bucket": "SHIP_AS_IS"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"file": "hooks/check_tailwind_wildcard_in_docs.sh",
|
|
22
|
+
"sourceHook": "check_tailwind_wildcard_in_docs.sh",
|
|
23
|
+
"bucket": "SHIP_AS_IS"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"file": "hooks/check_escape_marker_abuse.sh",
|
|
27
|
+
"sourceHook": "check_escape_marker_abuse.sh",
|
|
28
|
+
"bucket": "SHIP_REWRITTEN"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"file": "hooks/inject_deploy_url_after_push.sh",
|
|
32
|
+
"sourceHook": "inject_deploy_url_after_push.sh",
|
|
33
|
+
"bucket": "SHIP_REWRITTEN"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"PreToolUse": [
|
|
37
|
+
{
|
|
38
|
+
"file": "hooks/check_item_list_gap.sh",
|
|
39
|
+
"sourceHook": "check_item_list_gap.sh",
|
|
40
|
+
"bucket": "SHIP_AS_IS"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"file": "hooks/check_pattern_invariants.sh",
|
|
44
|
+
"sourceHook": "check_pattern_invariants.sh",
|
|
45
|
+
"bucket": "SHIP_AS_IS"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"file": "hooks/check_field_family_invariants.sh",
|
|
49
|
+
"sourceHook": "check_field_family_invariants.sh",
|
|
50
|
+
"bucket": "SHIP_AS_IS"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"file": "hooks/check_sidebar_menu_button_implicit_wrap.sh",
|
|
54
|
+
"sourceHook": "check_sidebar_menu_button_implicit_wrap.sh",
|
|
55
|
+
"bucket": "SHIP_AS_IS"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"file": "hooks/check_chrome_header_avatar_canonical.sh",
|
|
59
|
+
"sourceHook": "check_chrome_header_avatar_canonical.sh",
|
|
60
|
+
"bucket": "SHIP_AS_IS"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"file": "hooks/check_opacity_token_usage.sh",
|
|
64
|
+
"sourceHook": "check_opacity_token_usage.sh",
|
|
65
|
+
"bucket": "SHIP_REWRITTEN"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"file": "hooks/check_ds_anchor_preflight.sh",
|
|
69
|
+
"sourceHook": "check_ds_anchor_preflight.sh",
|
|
70
|
+
"bucket": "SHIP_REWRITTEN"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"file": "hooks/check_fork_product_quality.sh",
|
|
74
|
+
"sourceHook": "check_substantive_edit_approval_preflight.sh",
|
|
75
|
+
"bucket": "REPLACE"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|