1 Commits

Author SHA1 Message Date
0a1e622546 hotfix: guard staged asset commits 2026-05-25 15:33:30 +09:00
5 changed files with 137 additions and 6 deletions

10
.githooks/pre-commit Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v node >/dev/null 2>&1; then
if [ -s "$HOME/.nvm/nvm.sh" ]; then
. "$HOME/.nvm/nvm.sh"
fi
fi
node scripts/guard-staged-assets.mjs

8
.gitignore vendored
View File

@@ -23,6 +23,14 @@ node_modules.root-owned-backup/
.env.*
.tmp
!.env.example
tmp-*.png
tmp-*.jpg
tmp-*.jpeg
tmp-*.webp
tmp-*.gif
tmp-*.mp4
tmp-*.mov
tmp-*.webm
# etc workspace
etc/**/.env

View File

@@ -9,8 +9,8 @@
### Codex / AI 기본 규칙
* 사용자가 구현, 수정, 실행, 설정 변경, 문서 수정, 채팅 응답 개선을 요청하면 **현재 로컬 `main`에서 바로 작업**한다
* 브라우저 확인, 화면 검증, 접속 테스트, 기본 작업 도메인은 **`https://test.sm-home.cloud/` 하나만 기준으로 사용**한다
* 브라우저 스크린샷/캡처 검증은 기본적으로 **루트 `.env``CAPTURE_BASE_URL=https://test.sm-home.cloud/`와 등록 토큰(`CAPTURE_REGISTERED_ACCESS_TOKEN`, `VITE_ALLOWED_REGISTRATION_TOKEN`)을 사용해 토큰 등록 상태에서 진행**한다
* 브라우저 확인, 화면 검증, 접속 테스트, 기본 작업 도메인은 **`https://preview.sm-home.cloud/` 기준으로 사용**한다
* 브라우저 스크린샷/캡처 검증은 기본적으로 **루트 `.env``CAPTURE_BASE_URL=https://preview.sm-home.cloud/`와 등록 토큰(`CAPTURE_REGISTERED_ACCESS_TOKEN`, `VITE_ALLOWED_REGISTRATION_TOKEN`)을 사용해 토큰 등록 상태에서 진행**한다
* 별도로 운영 중인 `4173` 포트의 `ai-code-app-preview` 컨테이너는 **채팅 전용 테스트 서버가 아니라 현재 프로젝트 루트 기준 화면/기능 확인용 테스트 컨테이너**로 해석한다
* `test.sm-home.cloud` nginx 프록시는 **화면 `/``5174` 앱 테스트 서버로 보내고, `/api/``/ws/chat`은 항상 `127.0.0.1:3100` work-server로 유지**한다
* `test.sm-home.cloud``/api`, `/ws/chat`, `/.codex_chat`, `/public/.codex_chat` 프록시 대상은 사용자가 명시적으로 요청하지 않는 한 임의로 변경하지 않는다
@@ -18,9 +18,13 @@
* `채팅`, `메모 반영`, `문서 반영` 요청은 기본적으로 **브랜치 생성 없이 main 직접 수정**으로 해석한다
* `git remote`, `fetch`, `pull`, `push`, `branch`, `switch`, `checkout`, `merge`, `rebase`, `reset`, `stash`, `tag`, `commit` 같은 Git 관리 작업은 기본적으로 수행하지 않는다
* 사용자가 Git 작업을 **명시적으로 요청한 경우에만** 필요한 명령을 최소 범위로 수행한다
* Git 작업을 수행하더라도 `public/assets/` 아래 대용량 리소스, `tmp-*` 캡처 파일, 임시 산출물은 기본적으로 커밋 대상에 포함하지 않는다
* 이미지/동영상/PDF 같은 바이너리 자산이 정말 필요할 때만 예외적으로 커밋하고, 그 외에는 코드 변경과 분리해 별도 확인 후 처리한다
* 원격 저장소 연결 복구, 브랜치 전략 복구, release/main 동기화, 자동 merge 같은 작업을 자동으로 시도하지 않는다
* 현재는 브랜치 전략보다 **로컬 실행 가능 상태 유지, 코드 수정, 문서 갱신, 메모 반영 속도**를 우선한다
* `.auto_codex` 관련 Git 자동화, Plan 자동화, 브랜치 생성/병합 흐름은 사용자가 다시 요청하기 전까지 **비활성 상태**로 취급한다
* 사용자가 **명시적으로 요청한 경우를 제외하면** 구현 편의나 상태 갱신을 이유로 `polling`, `setInterval`, 주기적 재시도 루프 같은 반복 조회 구조를 추가하거나 유지하지 않는다
* 기존 기능에 `polling`, `setInterval`이 이미 있더라도 사용자 요청 없이 당연한 전제로 유지하지 말고, 이벤트 기반·명시적 새로고침·단발성 요청으로 대체 가능한지 먼저 검토한다
### 요청 해석 규칙
@@ -40,6 +44,7 @@
* 로컬 작업 중에도 사용자가 요청하지 않은 Git 정리 작업은 하지 않는다
* `reset`, `checkout`, `switch`, `clean`, 강제 덮어쓰기처럼 되돌리기 어려운 작업은 자동으로 수행하지 않는다
* 사용자가 Git 작업을 요청해도 **정말 필요한 범위만** 실행한다
* 임시 스크린샷, 테스트 캡처, 대용량 리소스 파일은 기본적으로 Git 커밋을 차단하고, 의도적 자산 커밋일 때만 명시적으로 예외 처리한다
* 현재 모드에서는 `main` 직접 수정이 허용되지만, **자동 commit / push는 여전히 금지**한다
---
@@ -47,7 +52,8 @@
## Codex Live / 채팅 / 작업 메모 규칙
* `Codex Live`, 일반 채팅 요청은 현재 프로젝트의 로컬 `main`을 기준으로 처리한다
* 외부 도메인 기준 동작 확인이 필요하면 기본 대상은 항상 `https://test.sm-home.cloud/`로 본다
* 외부 도메인 기준 동작 확인과 최종 검증은 기본적으로 `https://preview.sm-home.cloud/`를 우선 기준으로 본다
* `https://test.sm-home.cloud/`는 운영 비교나 프록시 점검이 꼭 필요할 때만 보조적으로 확인한다
* `https://test.sm-home.cloud/`에서 채팅/API 문제가 보이면 먼저 프런트 코드보다 **nginx의 `/api`와 `/ws/chat` 프록시가 `3100`을 가리키는지** 확인한다
* 채팅에서 나온 수정 요청도 별도 브랜치 생성 없이 바로 파일 수정으로 이어질 수 있다
* 일반 작업 메모는 기록 목적이면 메모로 유지하고, 실제 수정 지시라도 별도 자동화 접수가 아니면 `main` 기준 로컬 작업으로 연결한다
@@ -57,11 +63,21 @@
* 채팅 답변에서 링크 카드는 외부 공개 링크에만 사용하고 `[[link-card:제목|URL|버튼라벨]]` 형식을 정확히 지킨다
* 세션 리소스, 내부 문서, 코드, 로그, 테이블 파일처럼 `/api/chat/resources/...`, `/.codex_chat/...`, `/public/.codex_chat/...` 아래 내부 리소스에는 링크 카드를 사용하지 않는다
* 링크 카드 URL 안에 구분자 `|``%7C`로 인코딩하거나 `https:/api/...`처럼 잘못 줄여 쓰지 않는다
* `[[prompt:...]]` 내부의 `preview`, preview card 성격의 미리보기는 실제로 생성과 접근이 확인된 리소스에만 연결한다
* `[[prompt:...]]``preview.url`에는 로컬 파일 시스템 경로(`/home/...`, `C:\\...`)나 원본 `public/...` 경로를 직접 넣지 말고, 외부 `https://...` URL 또는 `/api/chat/resources/...`, `resource/...`, `/api/resource-manager/preview/...`처럼 실제 미리보기 가능한 경로만 사용한다
* `[[prompt:...]]`에서 내부 문서/HTML/표/산출물을 미리 보여줄 때는 링크 카드로 우회하지 말고 `preview.type:"resource"` 또는 용도에 맞는 preview 타입을 우선 사용한다
* `[[prompt:...]]`는 본문 문장 사이에 들어가더라도 prompt 컴포넌트로 안정적으로 파싱되어야 하며, HTML/문서 preview가 있는 prompt는 raw 텍스트로 남지 않게 확인한다
* prompt 안의 preview 리소스가 열리지 않을 때는 곧바로 `리소스를 못 찾겠다`고 답하지 말고, 먼저 파일 생성 여부, 세션 리소스 복사 여부, `preview.url` 문법, preview 타입 선택이 맞는지 순서대로 다시 확인한다
* 사용자가 `전체 케이스`, `모든 케이스`, `전수 검증`을 요청하면 실제 분기 함수를 직접 확인하고 `null` 반환, 기본값, 우선순위 예외까지 빠짐없이 표나 목록으로 정리한 뒤 검증했다고 답한다
* 전수 검증 요청에서 대표 예시 몇 개만 적거나 상태 enum 이름만 나열한 뒤 `전체 케이스`라고 단정하지 않는다
* HTML 미리보기 산출물은 fallback 안내문이나 앱 화면 URL로 대신하지 말고, 실제 `.html` 리소스를 만든 뒤 그 리소스가 열리는지 다시 확인하고 제공한다
* 모바일 캡처 결과나 최종 화면 검증 리소스는 링크 카드 대신 반드시 `[[preview:URL]]` 형식으로 제공한다
* 모바일 캡처 결과를 설명하는 본문 옆에 링크 카드를 덧붙이더라도, 같은 리소스의 `[[preview:URL]]` 표시는 생략하지 않는다
* 내부 문서성 리소스는 일반 경로나 자동 프리뷰 가능한 리소스 URL로 제공하고, 표 형태 확인이 필요하면 preview 컴포넌트에서 바로 열 수 있는 형식을 우선 사용한다
* markdown/document preview를 최대화해서 볼 때는 본문 배경과 텍스트 대비가 유지되도록 확인하고, 다크 배경 위에 검정 본문만 남는 상태로 두지 않는다
* 토큰 등록이 필요한 화면 검증이나 모바일 스크린샷은 **비로그인 기본 화면(Docs)으로 확인하지 말고**, `.env`에 저장된 등록 토큰을 브라우저 `localStorage`에 주입한 상태로 캡처한다
* 관리/등록 화면 UI를 만들 때는 버튼 클릭 후 뜨는 모달/드로어가 기존 하단 액션을 가리거나, 스크롤 마지막 입력이 잘리는 구조를 만들지 않는다. 데스크톱에서는 유사 항목을 한 줄에 묶고 좌우 여백을 적극 활용하며, 전역 헤더 우측 액션으로 바로 열 수 있는 기능은 중첩 모달보다 우선 검토한다
* `/play/apps` 아래에서 실행되는 앱 화면은 기본적으로 부모 앱 헤더를 다시 노출하지 말고, 개별 앱 콘텐츠가 화면을 가득 채우는 레이아웃을 우선 적용한다
---
@@ -76,7 +92,7 @@
## 한 줄 요약
👉 일반 채팅/수동 요청은 로컬 `main`에서 바로 수정한다
👉 외부 확인 기본 도메인은 `https://test.sm-home.cloud/` 하나
👉 외부 확인과 검증 기본 도메인은 `https://preview.sm-home.cloud/`
👉 자동화 브랜치 흐름은 문서에 고정 규칙으로 적지 않는다
👉 일반 Git 작업은 사용자가 명시적으로 요청할 때만 최소 범위로 한다

View File

@@ -10,6 +10,8 @@ React + Vite + TypeScript 기반의 문서/샘플 허브 애플리케이션입
- `채팅`, `Codex Live`, 일반 `작업메모`, `메모 반영` 요청도 같은 기준으로 해석합니다.
- 자동화 접수된 작업메모도 별도 브랜치 흐름을 이 문서에 고정하지 않고, 실제 운영 설정과 요청 문맥을 기준으로 처리합니다.
- 채팅 리소스와 첨부 파일은 `public/.codex_chat/<chat-session-id>/resource/...` 기준으로 제공하며, 업로드 파일은 `public/.codex_chat/<chat-session-id>/resource/uploads/...` 아래를 사용합니다.
- `public/assets/` 아래 리소스, `tmp-*` 캡처 파일, 대용량 바이너리 파일은 기본적으로 커밋하지 않습니다.
- 저장소에는 staged 자산 차단 훅이 연결되어 있으며, 의도적인 자산 커밋이 꼭 필요할 때만 `ALLOW_ASSET_COMMIT=1 git commit ...`으로 예외 처리합니다.
## 시작하기
@@ -37,8 +39,8 @@ docker compose -f docker-compose.preview.yml up -d --build
- 테스트용 컨테이너는 현재 `ai-code-app-preview` 하나만 유지합니다.
- 이 컨테이너는 채팅 전용 테스트가 아니라 **현재 프로젝트 루트 기준 화면/기능 확인용 테스트 컨테이너**로 사용합니다.
- 운영 프록시 확인은 `https://test.sm-home.cloud/` 기준으로 유지합니다.
- 소스 변경 검증과 최종 화면 확인은 `https://preview.sm-home.cloud/` 기준으로 진행합니다.
- 화면 테스트, 소스 변경 검증, 최종 화면 확인은 `https://preview.sm-home.cloud/` 기준으로 진행합니다.
- `https://test.sm-home.cloud/`는 운영 비교나 프록시 점검이 꼭 필요할 때만 보조적으로 확인합니다.
- 위 프록시 기준은 임의로 바꾸지 말고, 변경이 필요하면 반드시 운영 목적을 먼저 문서에 남깁니다.
- 임시 테스트 컨테이너를 추가로 띄우지 말고, 필요 시 기존 `ai-code-app-preview`만 재기동합니다.
- 재기동은 다른 서비스까지 건드리지 않고 아래 명령으로 해당 컨테이너만 처리합니다.

95
scripts/guard-staged-assets.mjs Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { statSync } from 'node:fs';
import path from 'node:path';
const repoRoot = process.cwd();
const allowAssetCommit = process.env.ALLOW_ASSET_COMMIT === '1';
const maxBinarySizeBytes = 5 * 1024 * 1024;
const tmpCapturePattern = /^tmp-.*\.(png|jpe?g|webp|gif|mp4|mov|webm)$/i;
const binaryAssetPattern = /\.(png|jpe?g|webp|gif|svg|mp4|mov|webm|pdf|zip|7z)$/i;
const blockedAssetPrefixes = ['public/assets/'];
function getStagedPaths() {
const output = execFileSync(
'git',
['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
{ cwd: repoRoot, encoding: 'utf8' }
);
return output
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
}
function toDisplaySize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function isBlockedAssetPath(filePath) {
return blockedAssetPrefixes.some((prefix) => filePath.startsWith(prefix));
}
function main() {
const stagedPaths = getStagedPaths();
const violations = [];
for (const relativePath of stagedPaths) {
const absolutePath = path.join(repoRoot, relativePath);
let stats;
try {
stats = statSync(absolutePath);
} catch {
continue;
}
if (!stats.isFile()) continue;
const baseName = path.basename(relativePath);
const isTmpCapture = tmpCapturePattern.test(baseName);
const isBinaryAsset = binaryAssetPattern.test(relativePath);
const isOversizedBinary = isBinaryAsset && stats.size > maxBinarySizeBytes;
const isBlockedAsset = isBinaryAsset && isBlockedAssetPath(relativePath);
if (!isTmpCapture && !isOversizedBinary && !isBlockedAsset) continue;
const reasons = [];
if (isTmpCapture) reasons.push('temporary capture file');
if (isBlockedAsset) reasons.push('asset path under public/assets');
if (isOversizedBinary) {
reasons.push(`binary file exceeds ${toDisplaySize(maxBinarySizeBytes)}`);
}
violations.push({
relativePath,
size: stats.size,
reasons,
});
}
if (violations.length === 0 || allowAssetCommit) {
return;
}
console.error('');
console.error('Blocked commit: staged asset files need an explicit override.');
console.error('Set ALLOW_ASSET_COMMIT=1 only when the asset commit is intentional.');
console.error('');
for (const violation of violations) {
console.error(
`- ${violation.relativePath} (${toDisplaySize(violation.size)}): ${violation.reasons.join(', ')}`
);
}
console.error('');
process.exit(1);
}
main();