/cc-coui-sync:status — 캔버스는 지금 어느 코드를 그리고 있나
Figma 플러그인이 지금 그리는 것이 어느 코드인지를 8행 표로 답합니다 — 번들에 구워진 소스 해시와 작업 트리·main 을 대조하고, components.json 신선도·main 에 도달하지 못한 흡수 브랜치·재촬영 잔여·마지막 Generate 시각·문서 사이트가 그리는 커밋·릴리스 대기를 함께 잽니다. 재생성했더니 컴포넌트 모습이 달라졌을 때,...
| 항목 | 내용 |
|---|---|
| 실행 명령 | /cc-coui-sync:status |
| 분류 | cocode design |
| 난이도 | ●●○ 보통 |
| MCP 서버 | figma |
한마디로#
디자인 시스템의 Figma 라이브러리는 코드에서 생성됩니다. 그래서 "지금 캔버스에 보이는 것"은 누군가의 컴퓨터에 빌드돼 있는 코드의 사진입니다. 이 커맨드는 그 사진이 어느 시점의 코드인지를 재서 표 하나로 보여줍니다 — 아무것도 고치지 않고 읽기만 합니다.
누가·언제 쓰나요#
- 재생성했더니 컴포넌트 모습이 예전으로 돌아갔을 때 — 원인이 대개 여기 있습니다
- 흡수 작업을 시작하기 전(무엇이 이미 반영돼 있나) · 끝낸 뒤(정말 도달했나)
- "코드와 캔버스가 맞나요?" 라는 질문에 인상이 아니라 근거로 답해야 할 때
왜 이 커맨드가 있나#
코드가 정본이고 캔버스는 재생성물인 구조에서, "머지됐는가 / 빌드됐는가 / 릴리즈됐는가" 는 서로 다른 세 질문인데 화면에는 구별되어 나타나지 않습니다. 실제로 두 달치 흡수 작업이 머지되지 않은 브랜치에 살아 있는 동안 플러그인은 그 브랜치에서 빌드되어 있었고, 그 상태는 "재생성했더니 컴포넌트 모습이 달라졌다" 라는 질문으로만 드러났습니다 — 원인 추적에 브랜치별 해시 대조가 필요했습니다. 이 표가 있었으면 그 질문은 질문이 되기 전에 화면에 있었습니다.
릴리즈는 캔버스에 한 픽셀도 영향을 주지 않습니다 — 플러그인은 배포 패키지가 아니라 그 컴퓨터의 저장소를 읽습니다. 그런데도 표에 릴리스와 문서 사이트 행이 있는 이유는, 디자이너의 "머지했는데 반영된 거냐" 에 답이 셋이기 때문입니다: 캔버스(재빌드 + Generate) · 문서 사이트(Deploy 는 수동) · 소비 앱(릴리스 태그). 셋 중 하나만 보고 "반영됐다" 고 믿는 것이 이 표가 막으려는 두 번째 오해입니다.
실행 위치#
coco-de/coui 레포 루트. 이 커맨드가 세는 값은 전부 그 레포의 것입니다 — 다른 레포에서 돌리면 경로가 없어 아무것도 재지 못합니다. Figma MCP 는 5단계에만 필요하며, 없으면 그 행만
측정 불가 로 표기하고 계속합니다.
이 파일은 절차만 권위를 가집니다. 엔지니어링 룰의 정본은 coui 레포의
.claude/rules/이며(매 세션 자동 로드) 충돌 시 그쪽이 우선합니다.
1. 번들 출처 — code.js 는 어느 소스에서 빌드됐나#
번들에 구워진 해시를 꺼낸다 (code.js 는 gitignore 대상이라 없을 수 있다):
grep -oE 'kSharedSourceHash = true \? "[0-9a-f]{16}"' tools/figma-token-sync/code.js \
| grep -oE '[0-9a-f]{16}'
- 파일이 없으면 →
빌드된 적 없음 — Generate 를 돌리려면 npm run build 필요로 판정하고 다음 단계로. -
매치가 없으면 →
dev 폴백 — 주입 없이 빌드됨, 무효화 죽음으로 판정한다. 이 상태는check_shared_source_hash.sh가 CI 에서 막는 바로 그것이다.
이제 후보 ref 들의 소스 해시를 계산해 대조한다. 계산식은 빌드 스크립트(tools/figma-token-sync/package.json 의 build)와
글자 그대로 같아야 한다 — 다른 식으로 재구현하면 두 계산이 갈라지는 날 이 표가 거짓을 말한다:
# 작업 트리
( cd tools/figma-token-sync && \
find src code.ts -name '*.ts' -not -path 'src/data/*' | sort | xargs cat | shasum -a 256 | cut -c1-16 )
# origin/main — 작업 트리를 건드리지 않는 임시 worktree 로
git fetch origin main -q
git worktree add --detach /tmp/fss-main origin/main -q 2>/dev/null || true
( cd /tmp/fss-main/tools/figma-token-sync && \
find src code.ts -name '*.ts' -not -path 'src/data/*' | sort | xargs cat | shasum -a 256 | cut -c1-16 )
git worktree remove --force /tmp/fss-main
왜 worktree 인가: git stash / git checkout 으로 오가는 대조는 사용자의 작업 트리를 건드린다 — 이 커맨드는 읽기 전용이라는 약속을 그 방식으로는 지킬 수 없다.
판정: 구운 해시가 작업 트리와 일치 / origin/main 과 일치 / 둘 다 아님. 셋째가 가장 중요한 답이다 — 캔버스가 지금 저장소 어디에도 없는(혹은 다른 브랜치의) 코드로 그려지고 있다는 뜻이므로, 그때는 로컬 브랜치들을 같은 식으로 순회해 어느 ref 인지까지 찾아 적는다.
src/data/는 계산에서 빠진다 — 즉 이 해시는 빌더 로직의 출처만 답한다. 데이터(components.json)의 신선도는 2단계가 따로 답한다. 둘은 독립적으로 낡을 수 있고, 실제로 서로 다른 가드가 지킨다.
2. components.json — 데이터는 신선한가#
bash scripts/guards/structure/check_figma_export_fresh.sh
통과/실패를 그대로 적는다. 실패면 처방도 가드 출력에 있다 (export 재실행).
3. main 미도달 흡수 커밋 — 좌초 감지#
현재 브랜치와, 원격의 다른 브랜치들 중 컴포넌트/플러그인 경로를 건드리고 main 에 없는 커밋을 센다:
git fetch origin -q
# 현재 브랜치
git log --oneline origin/main..HEAD -- \
packages/coui_core packages/coui_flutter packages/coui_web tools/figma-token-sync | cat
# 원격 브랜치 — 최근 30일 내 움직였고, 그 브랜치의 PR 상태로 분류
gh pr list --state all --limit 1000 --json headRefName,state,number > /tmp/fss-prs.json
cutoff=$(( $(date +%s) - 30*24*3600 ))
git for-each-ref --format='%(committerdate:unix) %(refname:short)' refs/remotes/origin \
| while read -r ts b; do
[ "$ts" -lt "$cutoff" ] && continue
case "$b" in origin/main|origin/HEAD|origin/chore/preset-baseline-*|origin/chore/golden-baseline-*) continue;; esac
n=$(git rev-list --count origin/main.."$b" -- \
packages/coui_core packages/coui_flutter packages/coui_web tools/figma-token-sync 2>/dev/null)
[ "${n:-0}" -gt 0 ] || continue
short=${b#origin/}
info=$(jq -r --arg h "$short" \
'[.[] | select(.headRefName==$h)]
| if length==0 then "NONE"
else (sort_by(.state=="OPEN") | last | "\(.state) #\(.number)") end' /tmp/fss-prs.json)
case "$info" in
MERGED*|CLOSED*) ;; # 랜딩됐거나 폐기됨 — 죽은 브랜치
OPEN*) echo "진행중 $b: $n ($info)";;
NONE) echo "좌초⚠️ $b: $n (PR 없음)";;
esac
done
왜 두 필터인가: 이 레포에는 끝난 작업의 브랜치가 수백 개 남아 있다. 날짜 컷 하나만으로는 부족했다 — 실측에서 30일 컷이 178줄을 남겼고 그중 살아 있는 것은 4줄이었다(머지된 PR 의 브랜치가 삭제되지 않고 남으면 committerdate 는 최근이다). 브랜치가 살아 있느냐는 PR 상태가 직접 답한다: MERGED/CLOSED 면 죽은 것이고, OPEN 이면 진행 중, PR 자체가 없으면 그것이 좌초다. 같은 브랜치 이름으로 PR 이 여럿이면(재타겟 실패로 다시 연 경우) OPEN 을 우선한다.
왜 세는가: 좌초된 흡수는 스스로 알리지 않는다. 브랜치에서 빌드해 Generate 를 돌리는 동안은 캔버스가 옳아 보이고, 누군가 main 에서 다시 빌드하는 순간 되돌아간다 — 그 되돌아감이 이 표가 미리 보여주려는 것이다.
PR 없음 과 OPEN 은 다르게 읽는다: 전자는 아무도 리뷰를 기다리지 않는 진짜 좌초, 후자는 랜딩 대기 중이라 곧 해소될 상태.
4. 재촬영 잔여 — dispatch 됐지만 아직 안 실린 baseline#
for b in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin \
| grep -E 'origin/chore/(preset|golden)-baseline-'); do
# 그 브랜치의 승격 커밋 패치가 main 에 이미 있으면(cherry-pick 완료) 정리 대상일 뿐이다
if [ "$(git cherry origin/main "$b" | grep -c '^+')" -gt 0 ]; then
echo "$b: 미수용 — cherry-pick 대기"
fi
done
재촬영 워크플로(preset-baseline.yml · golden-baseline.yml) 는 브랜치를 밀어두고 사람이 옮기는 구조라, 잊힌 산출물이 원격에 조용히 쌓일 수 있다. 이 행이 그것을 센다.
5. 마지막 Generate 발자국 — (Figma MCP 가용 시)#
실행은 자기 위치를 문서에 남긴다 — figma.root 의 sharedPluginData, 네임스페이스 coui, 키 last-step, 값
단계|epoch-ms. use_figma 로 읽는다 (읽기 전용 스크립트, 반드시 figma-use 스킬 로드 후):
const v = figma.root.getSharedPluginData('coui', 'last-step');
return { lastStep: v || null };
값을 단계 · <사람이 읽는 시각> 으로 표기한다. 비어 있으면 발자국 없음 (이 문서에서 Generate 이력 없음). MCP 가 없으면
측정 불가 (Figma MCP 미연결).
왜: "마지막으로 캔버스를 그린 게 언제·어디까지였나" 는 저장소 어디에도 없고 문서에만 있다. 죽은 실행의 마지막 위치를 읽으라고 만들어진 값이지만, 살아 끝난 실행의 시각 증거로도 그대로 쓰인다.
6. 로컬 SDK — 이 머신에서 무엇이 가능한가#
flutter --version 2>/dev/null | head -1
grep -m1 'flutter:' pubspec.yaml
로컬이 워크스페이스 floor 미만이면: 로컬 flutter test 불가 — 검증은 CI · 재촬영은 workflow dispatch. 이 한 줄이 있어야 "왜 로컬에서 안 도냐" 를 매번 다시 밟지 않는다.
7. 문서 사이트 — coui.cocode.im 은 어느 커밋을 그리나#
deploy.yaml 은 수동 실행 전용이다(hosted 러너 과금 때문에 매 머지마다 돌리지 않기로 한 결정 — 워크플로 파일 첫 주석이 정본). 그래서 머지는 사이트를 바꾸지 않는다.
gh run list --workflow=deploy.yaml --status=success --limit=1 --json headSha,updatedAt \
| jq -r '.[0] | "\(.headSha[0:9]) \(.updatedAt)"'
git fetch origin main -q
git rev-list --count <위 headSha>..origin/main
값은 <sha> · <시각> · main 보다 N 커밋 뒤 로 적는다. 판정은 항상 ℹ️ — 뒤처짐이 설계이므로 ⚠️ 가 아니다. 다만 N 이 0 이 아니면 다음 한 걸음을 붙인다:
Actions → Deploy → Run workflow (ref: main). 성공 실행이 없으면 배포 이력 없음.
왜: "머지하고 Generate 해서 Figma 에 잘 나오는 걸 확인했는데 사이트는 옛날 모양" 은 결함이 아니라 이 행의 값이다. 행이 없으면 그 질문은 매번 워크플로 파일을 열어 답해야 한다.
8. 릴리스 — 소비 앱에는 갔나#
소비 앱은 pub.dev 가 아니라 git 태그로 참조한다. main 푸시마다 릴리스 PR 이 자동으로 열리고, 그 PR 을 머지해야 태그가 찍히며, 그 뒤 앱이 참조를 올려야 도착한다.
gh pr list --state open --search 'label:"autorelease: pending"' --json number,title \
| jq -r '.[] | "#\(.number) \(.title)"'
gh release list --limit 1 | cut -f1,3
값은 릴리스 PR #N 대기 / 대기 없음 · 최신 태그 <tag> (<날짜>) 로 적는다. 판정 ℹ️. 릴리스 PR 이 열려 있으면 다음 한 걸음:
그 PR 이 초록인지 보고 머지 — 머지 전까지 소비 앱은 이 흡수를 받을 수 없다. 라벨로 안 잡히면 제목 chore(main): release
로 찾는다.
출력 형식#
측정을 마치면 이 표 하나로 보고한다. 각 행은 위 단계의 실측값이어야 하며, 실행하지 않은 단계를 추정으로 채우지 않는다:
| 항목 | 값 | 판정 |
|----------------------|----------------------------|------|
| 번들 출처 | <hash> = <ref> / 불일치 | ✅/⚠️ |
| components.json | fresh / stale | ✅/⚠️ |
| main 미도달 흡수 | N 커밋 (<브랜치>) | ✅/⚠️ |
| 재촬영 잔여 | 없음 / <브랜치> | ✅/⚠️ |
| 마지막 Generate | <단계> · <시각> / 측정 불가 | ℹ️ |
| 로컬 SDK | <버전> (floor <요구>) | ✅/⚠️ |
| 문서 사이트 | <sha> · main 보다 N 커밋 뒤 | ℹ️ |
| 릴리스 | PR #N 대기 / 없음 · 태그 <tag> | ℹ️ |
⚠️ 가 하나라도 있으면 그 행 밑에 다음 한 걸음(정확한 명령 또는 어느 커맨드로 이어지는지 — 흡수 잔여면 /cc-coui-sync:absorb-apply)을 한 줄로 붙인다.
표 아래에 디자이너용 세 줄을 덧붙인다 — 표를 읽지 않아도 "반영됐나" 의 세 답이 보이게:
캔버스 = main? ✅/⚠️ (1·2행 — 아니면: main 카드에서 npm run build → 플러그인 재열기 → Generate)
사이트 = main? ✅/ℹ️ (7행 — 아니면: Actions → Deploy)
앱 = 최신 태그? ✅/ℹ️ (8행 — 아니면: 릴리스 PR 머지 → 앱 참조 갱신)