/cc-coui-sync:status — 캔버스는 지금 어느 코드를 그리고 있나#
| 항목 | 내용 |
|---|---|
| 실행 명령 | /cc-coui-sync:status |
| 분류 | cocode design |
| 난이도 | ●●○ 보통 |
| MCP 서버 | figma |
한마디로#
디자인 시스템의 Figma 라이브러리는 코드에서 생성됩니다. 그래서 "지금 캔버스에 보이는 것"은 누군가의 컴퓨터에 빌드돼 있는 코드의 사진입니다. 이 커맨드는 그 사진이 어느 시점의 코드인지를 재서 표 하나로 보여줍니다 — 아무것도 고치지 않고 읽기만 합니다.
누가·언제 쓰나요#
- 재생성했더니 컴포넌트 모습이 예전으로 돌아갔을 때 — 원인이 대개 여기 있습니다
- 흡수 작업을 시작하기 전(무엇이 이미 반영돼 있나) · 끝낸 뒤(정말 도달했나)
- "코드와 캔버스가 맞나요?" 라는 질문에 인상이 아니라 근거로 답해야 할 때
왜 이 커맨드가 있나#
코드가 정본이고 캔버스는 재생성물인 구조에서, "머지됐는가 / 빌드됐는가 / 릴리즈됐는가" 는 서로 다른 세 질문인데 화면에는 구별되어 나타나지 않습니다. 실제로 두 달치 흡수 작업이 머지되지 않은 브랜치에 살아 있는 동안 플러그인은 그 브랜치에서 빌드되어 있었고, 그 상태는 "재생성했더니 컴포넌트 모습이 달라졌다" 라는 질문으로만 드러났습니다 — 원인 추적에 브랜치별 해시 대조가 필요했습니다. 이 표가 있었으면 그 질문은 질문이 되기 전에 화면에 있었습니다.
(릴리즈는 이 표에 없습니다 — 플러그인은 배포 패키지가 아니라 그 컴퓨터의 저장소를 읽으므로, 릴리즈 여부는 캔버스에 한 픽셀도 영향을 주지 않습니다.)
실행 위치#
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. 이 한 줄이 있어야 "왜 로컬에서 안 도냐" 를 매번 다시 밟지 않는다.
출력 형식#
측정을 마치면 이 표 하나로 보고한다. 각 행은 위 단계의 실측값이어야 하며, 실행하지 않은 단계를 추정으로 채우지 않는다:
| 항목 | 값 | 판정 |
|----------------------|----------------------------|------|
| 번들 출처 | < hash > = < ref > / 불일치 | ✅/⚠️ |
| components.json | fresh / stale | ✅/⚠️ |
| main 미도달 흡수 | N 커밋 ( < 브랜치 > ) | ✅/⚠️ |
| 재촬영 잔여 | 없음 / < 브랜치 > | ✅/⚠️ |
| 마지막 Generate | < 단계 > · < 시각 > / 측정 불가 | ℹ️ |
| 로컬 SDK | < 버전 > (floor < 요구 > ) | ✅/⚠️ |
⚠️ 가 하나라도 있으면 그 행 밑에 다음 한 걸음(정확한 명령 또는 어느 커맨드로 이어지는지 — 흡수 잔여면 /cc-coui-sync:absorb-apply)을 한 줄로 붙인다.