370 lines
13 KiB
JavaScript
Executable File
370 lines
13 KiB
JavaScript
Executable File
#!/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();
|