#!/usr/bin/env node /** * dreaming.js - "AI 코더"에서 "상태 저장형 컨벤션 가디언"으로 (Dreaming의 힘) * * 이 스크립트는 프로젝트 루트(CWD)의 코드베이스를 휴리스틱하게 스캔하여, * 현재 프레임워크 상태, 액티브 Pinia 스토어, 커스텀 Composable, 컴포넌트 구조, 테일윈드 설정 등을 추출합니다. * 분석된 내용은 .claude/project/dreaming-context.md 파일로 기록되어, * Claude Code가 프로젝트의 최신 컨벤션과 아키텍처 상태를 항시 보존하고 인지하도록 돕습니다. */ const fs = require('fs'); const path = require('path'); const CWD = process.cwd(); const CLAUDE_DIR = path.join(CWD, '.claude'); const PROJECT_DIR = path.join(CLAUDE_DIR, 'project'); const OUTPUT_FILE = path.join(PROJECT_DIR, 'dreaming-context.md'); const CLAUDE_MD = path.join(CWD, 'CLAUDE.md'); // 헬퍼: 디렉토리 존재 여부 확인 function directoryExists(dirPath) { try { return fs.statSync(dirPath).isDirectory(); } catch (e) { return false; } } // 헬퍼: 파일 존재 여부 확인 function fileExists(filePath) { try { return fs.statSync(filePath).isFile(); } catch (e) { return false; } } // 헬퍼: 재귀적으로 파일 목록 가져오기 (옵션 포함) function getFilesRecursive(dirPath, extFilter = [], ignoreDirs = ['node_modules', '.git', '.nuxt', 'dist']) { let results = []; if (!directoryExists(dirPath)) return results; const list = fs.readdirSync(dirPath); list.forEach((file) => { const fullPath = path.join(dirPath, file); const stat = fs.statSync(fullPath); if (stat && stat.isDirectory()) { if (!ignoreDirs.includes(file)) { results = results.concat(getFilesRecursive(fullPath, extFilter, ignoreDirs)); } } else { const ext = path.extname(file); if (extFilter.length === 0 || extFilter.includes(ext)) { results.push(fullPath); } } }); return results; } // 1. package.json 분석 function analyzePackageJson() { const packagePath = path.join(CWD, 'package.json'); if (!fileExists(packagePath)) { return { name: 'Unknown Project', framework: 'Unknown', techStack: [], scripts: [] }; } try { const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const deps = { ...pkg.dependencies, ...pkg.devDependencies }; const techStack = []; let framework = 'Vue/Nuxt'; if (deps['nuxt'] || deps['nuxt3'] || deps['nuxt-edge']) { framework = `Nuxt (${deps['nuxt'] || deps['nuxt3'] || 'v3'})`; techStack.push('Nuxt'); } else if (deps['vue']) { framework = `Vue (${deps['vue']})`; techStack.push('Vue'); } else if (deps['next']) { framework = `Next.js (${deps['next']})`; techStack.push('Next.js'); } else if (deps['react']) { framework = `React (${deps['react']})`; techStack.push('React'); } if (deps['pinia'] || deps['@pinia/nuxt']) { techStack.push('Pinia (상태 관리)'); } if (deps['tailwindcss'] || deps['@nuxtjs/tailwindcss']) { techStack.push('Tailwind CSS (스타일)'); } if (deps['typescript']) { techStack.push('TypeScript'); } if (deps['vitest'] || deps['@vitest/ui']) { techStack.push('Vitest (유닛 테스트)'); } if (deps['eslint']) { techStack.push('ESLint'); } if (deps['prettier']) { techStack.push('Prettier'); } return { name: pkg.name || 'Unnamed Project', version: pkg.version || '1.0.0', framework, techStack, scripts: pkg.scripts ? Object.keys(pkg.scripts) : [] }; } catch (e) { return { name: 'Parsing Error', framework: 'Unknown', techStack: [], scripts: [], error: e.message }; } } // 2. 디렉토리 구조 스캔 및 요약 function scanDirectoryStructure() { const dirsToScan = ['components', 'composables', 'stores', 'pages', 'server', 'layouts', 'middleware', 'plugins', 'types', 'assets']; const summary = {}; dirsToScan.forEach((dirName) => { const dirPath = path.join(CWD, dirName); if (directoryExists(dirPath)) { const files = getFilesRecursive(dirPath); summary[dirName] = { exists: true, count: files.length, examples: files.slice(0, 5).map(f => path.relative(CWD, f)) }; } else { summary[dirName] = { exists: false, count: 0, examples: [] }; } }); return summary; } // 3. Pinia 스토어 상세 분석 function analyzePiniaStores() { const storesDir = path.join(CWD, 'stores'); const stores = []; if (!directoryExists(storesDir)) { // composables 내에 스토어가 정의되어 있을 수도 있으므로 추가 탐색 가능 return stores; } const files = getFilesRecursive(storesDir, ['.ts', '.js']); files.forEach((file) => { try { const content = fs.readFileSync(file, 'utf8'); const filename = path.basename(file); // defineStore 매칭 const defineStoreMatch = content.match(/defineStore\(\s*['"`]([^'"`]+)['"`]/); const storeId = defineStoreMatch ? defineStoreMatch[1] : null; // 상태(state) 필드 휴리스틱 추출 const stateFields = []; const stateRegex = /const\s+([a-zA-Z0-9_$]+)\s*=\s*(ref|reactive|computed)/g; let match; while ((match = stateRegex.exec(content)) !== null) { stateFields.push(`${match[1]} (${match[2]})`); } // 함수(actions) 추출 const actionFields = []; const actionRegex = /function\s+([a-zA-Z0-9_$]+)/g; while ((match = actionRegex.exec(content)) !== null) { if (!match[1].startsWith('use')) { actionFields.push(match[1]); } } stores.push({ file: path.relative(CWD, file), id: storeId || filename.replace(path.extname(filename), ''), state: stateFields, actions: actionFields }); } catch (e) { // ignore } }); return stores; } // 4. 커스텀 Composable 분석 function analyzeComposables() { const composablesDir = path.join(CWD, 'composables'); const composables = []; if (!directoryExists(composablesDir)) return composables; const files = getFilesRecursive(composablesDir, ['.ts', '.js']); files.forEach((file) => { try { const content = fs.readFileSync(file, 'utf8'); const filename = path.basename(file); const relativePath = path.relative(CWD, file); // export const useXxx 함수 매칭 const useFuncRegex = /export\s+const\s+(use[a-zA-Z0-9_$]+)/g; const useFuncs = []; let match; while ((match = useFuncRegex.exec(content)) !== null) { useFuncs.push(match[1]); } const defaultFuncRegex = /export\s+default\s+function\s+(use[a-zA-Z0-9_$]+)/; const defaultMatch = content.match(defaultFuncRegex); if (defaultMatch) { useFuncs.push(defaultMatch[1]); } if (useFuncs.length > 0) { composables.push({ file: relativePath, functions: useFuncs }); } else { // 파일명이 useXxx 형태인 경우 추가 if (filename.startsWith('use')) { composables.push({ file: relativePath, functions: [filename.replace(path.extname(filename), '')] }); } } } catch (e) { // ignore } }); return composables; } // 5. 테스트 파일 통계 function analyzeTests() { const testFiles = getFilesRecursive(CWD, ['.spec.ts', '.spec.js', '.test.ts', '.test.js']); return { count: testFiles.length, files: testFiles.slice(0, 10).map(f => path.relative(CWD, f)) }; } // 메인 실행기 function run() { console.log('🤖 프로젝트 "Dreaming" 컨텍스트 분석 시작...'); const pkgInfo = analyzePackageJson(); const dirSummary = scanDirectoryStructure(); const stores = analyzePiniaStores(); const composables = analyzeComposables(); const tests = analyzeTests(); const timestamp = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }); // 마크다운 문서 빌드 let md = `# 🧠 프로젝트 자율 인지 메모리 (Dreaming Context) 이 파일은 \`dreaming.js\` 스크립트에 의해 프로젝트 코드베이스를 분석하여 자동 생성되었습니다. Claude Code가 프로젝트의 실시간 코드 구조, 사용 중인 스토어, 컴포넌트 레이아웃, 그리고 최신 개발 흐름을 완벽히 인지하도록 돕습니다. * **최종 동기화 시간:** ${timestamp} (Asia/Seoul) --- ## 🏗 프로젝트 정보 * **프로젝트명:** \`${pkgInfo.name}\` (v${pkgInfo.version || '1.0.0'}) * **핵심 프레임워크:** \`${pkgInfo.framework}\` * **기술 스택 라이브러리:** ${pkgInfo.techStack.map(tech => ` - ${tech}`).join('\n') || ' - (감지된 주요 라이브러리 없음)'} --- ## 📁 디렉토리 구조 및 컴포넌트 현황 현재 활성화되어 있는 프로젝트 레이아웃 정보입니다. | 디렉토리 | 활성 여부 | 파일 개수 | 주요 샘플 파일 (최대 5개) | |---|---|---|---| ${Object.entries(dirSummary).map(([name, info]) => { return `| \`${name}/\` | ${info.exists ? '✅' : '❌'} | ${info.count}개 | ${info.examples.map(ex => `\`${path.basename(ex)}\``).join(', ') || '-'} |`; }).join('\n')} --- ## 🍍 액티브 Pinia 스토어 목록 현재 코드베이스에 존재하는 글로벌 상태 저장소들의 템플릿 정보입니다. 새 기능을 개발할 때 아래 스토어를 재사용하거나 참고하세요. ${stores.length === 0 ? '*감지된 Pinia 스토어가 없습니다. (stores/ 디렉토리 없음 혹은 비어있음)*' : stores.map(store => { return `### 📦 \`${store.id}\` * **정의 파일:** \`${store.file}\` * **감지된 상태 (state/computed):** ${store.state.length > 0 ? store.state.map(s => `\`${s}\``).join(', ') : '없음'} * **감지된 액션 (actions/methods):** ${store.actions.length > 0 ? store.actions.map(a => `\`${a}\``).join(', ') : '없음'} `; }).join('\n')} --- ## 🎣 커스텀 Composable (useXxx) 목록 다양한 비즈니스 로직과 부수효과를 격리해 둔 커스텀 훅 목록입니다. 컴포넌트 내부에서 비즈니스 로직을 직접 짜기 전, 아래 훅들의 재사용 가능성을 먼저 타진하세요. ${composables.length === 0 ? '*감지된 커스텀 Composable이 없습니다. (composables/ 디렉토리 없음 혹은 비어있음)*' : composables.map(comp => { return `- **파일:** \`${comp.file}\` - **제공 함수:** ${comp.functions.map(f => `\`${f}()\``).join(', ')} `; }).join('\n')} --- ## 🧪 유닛 테스트 통계 현재까지 구축된 테스트 커버리지 현황입니다. * **감지된 테스트 파일 수:** \`${tests.count}개\` ${tests.count > 0 ? `* **최근 테스트 목록:**\n${tests.files.map(f => ` - \`${f}\``).join('\n')}` : ' *(새 기능을 추가할 때 반드시 Vitest 규격의 유닛 테스트를 함께 작성해야 함)*'} --- ## 🛠 실행 가능한 스크립트 (package.json) 프로젝트 구동 및 테스트 검증을 위해 사용 가능한 명령어 리스트입니다. ${pkgInfo.scripts.map(s => `- \`npm run ${s}\` (또는 pnpm/yarn/bun)`).join('\n') || '- 스크립트 없음'} `; // 디렉토리 및 파일 저장 if (!directoryExists(CLAUDE_DIR)) { fs.mkdirSync(CLAUDE_DIR); } if (!directoryExists(PROJECT_DIR)) { fs.mkdirSync(PROJECT_DIR); } fs.writeFileSync(OUTPUT_FILE, md, 'utf8'); console.log(`✅ Dreaming Context 업데이트 완료! -> ${path.relative(CWD, OUTPUT_FILE)}`); // CLAUDE.md에 자동 임포트 추가 처리 if (fileExists(CLAUDE_MD)) { let claudeMdContent = fs.readFileSync(CLAUDE_MD, 'utf8'); const importStr = '@.claude/project/dreaming-context.md'; if (!claudeMdContent.includes(importStr)) { // '## 프로젝트 지침' 혹은 '## 공통 지침' 섹션 밑에 삽입 시도 const sectionMatch = claudeMdContent.match(/(## 프로젝트 지침\r?\n)/); if (sectionMatch) { claudeMdContent = claudeMdContent.replace( sectionMatch[0], `${sectionMatch[0]}${importStr}\n` ); fs.writeFileSync(CLAUDE_MD, claudeMdContent, 'utf8'); console.log(`🔗 CLAUDE.md에 ${importStr} 동적 임포트 구문을 연결했습니다.`); } else { // 찾을 수 없다면 파일 상단 혹은 하단에 단순 추가 claudeMdContent = claudeMdContent + `\n\n## 자동 분석 컨텍스트\n${importStr}\n`; fs.writeFileSync(CLAUDE_MD, claudeMdContent, 'utf8'); console.log(`🔗 CLAUDE.md 끝에 ${importStr} 동적 임포트 구문을 추가했습니다.`); } } } else { console.log(`⚠️ 프로젝트 루트에 CLAUDE.md 가 존재하지 않습니다. CLAUDE.md를 먼저 생성하고 @.claude/project/dreaming-context.md 임포트 선언을 수동으로 추가하는 것을 권장합니다.`); } } // 스크립트 실행 run();