docuking-mcp 2.5.7 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/docs.js +519 -0
- package/handlers/index.js +34 -0
- package/handlers/kingcast.js +569 -0
- package/handlers/sync.js +1501 -0
- package/handlers/validate.js +544 -0
- package/handlers/version.js +102 -0
- package/index.js +149 -2762
- package/lib/config.js +114 -0
- package/lib/files.js +212 -0
- package/lib/index.js +41 -0
- package/lib/init.js +270 -0
- package/lib/utils.js +74 -0
- package/package.json +1 -1
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocuKing MCP - 킹캐스트 핸들러 모듈
|
|
3
|
+
* 오너의 Policy/Config를 협업자에게 자동 배포하는 시스템
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getLocalConfig,
|
|
12
|
+
getAiBasePath,
|
|
13
|
+
} from '../lib/config.js';
|
|
14
|
+
|
|
15
|
+
// 킹캐스트 대상 폴더
|
|
16
|
+
const KINGCAST_FOLDERS = ['_Infra_Config', '_Policy'];
|
|
17
|
+
|
|
18
|
+
// 킹캐스트 해시 저장 파일
|
|
19
|
+
const KINGCAST_HASH_FILE = '.docuking/kingcast_hashes.json';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 파일 해시 계산
|
|
23
|
+
*/
|
|
24
|
+
function calculateFileHash(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(filePath);
|
|
27
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 킹캐스트 대상 파일 수집
|
|
35
|
+
* yy_All_Docu/_Infra_Config/, yy_All_Docu/_Policy/ 내의 모든 파일
|
|
36
|
+
*/
|
|
37
|
+
function collectKingcastFiles(localPath) {
|
|
38
|
+
const allDocuPath = path.join(localPath, 'yy_All_Docu');
|
|
39
|
+
const files = {};
|
|
40
|
+
|
|
41
|
+
for (const folder of KINGCAST_FOLDERS) {
|
|
42
|
+
const folderPath = path.join(allDocuPath, folder);
|
|
43
|
+
if (!fs.existsSync(folderPath)) continue;
|
|
44
|
+
|
|
45
|
+
const collectRecursive = (dir, relativePath = '') => {
|
|
46
|
+
const items = fs.readdirSync(dir);
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
const fullPath = path.join(dir, item);
|
|
49
|
+
const relPath = relativePath ? `${relativePath}/${item}` : item;
|
|
50
|
+
const stat = fs.statSync(fullPath);
|
|
51
|
+
|
|
52
|
+
if (stat.isDirectory()) {
|
|
53
|
+
collectRecursive(fullPath, relPath);
|
|
54
|
+
} else {
|
|
55
|
+
// 모든 파일 수집 (.md뿐 아니라 .env, .json 등 포함)
|
|
56
|
+
const key = `${folder}/${relPath}`;
|
|
57
|
+
files[key] = {
|
|
58
|
+
fullPath,
|
|
59
|
+
hash: calculateFileHash(fullPath),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
collectRecursive(folderPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 저장된 킹캐스트 해시 로드
|
|
73
|
+
*/
|
|
74
|
+
function loadKingcastHashes(localPath) {
|
|
75
|
+
const hashFilePath = path.join(localPath, KINGCAST_HASH_FILE);
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(hashFilePath)) {
|
|
78
|
+
return JSON.parse(fs.readFileSync(hashFilePath, 'utf-8'));
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// 파싱 실패 시 빈 객체 반환
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 킹캐스트 해시 저장
|
|
88
|
+
*/
|
|
89
|
+
function saveKingcastHashes(localPath, hashes) {
|
|
90
|
+
const hashFilePath = path.join(localPath, KINGCAST_HASH_FILE);
|
|
91
|
+
const dir = path.dirname(hashFilePath);
|
|
92
|
+
if (!fs.existsSync(dir)) {
|
|
93
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
fs.writeFileSync(hashFilePath, JSON.stringify(hashes, null, 2), 'utf-8');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 변경 감지: 추가/수정/삭제된 파일 찾기
|
|
100
|
+
*/
|
|
101
|
+
function detectChanges(localPath) {
|
|
102
|
+
const currentFiles = collectKingcastFiles(localPath);
|
|
103
|
+
const savedHashes = loadKingcastHashes(localPath);
|
|
104
|
+
|
|
105
|
+
const changes = {
|
|
106
|
+
added: [], // 새로 추가된 파일
|
|
107
|
+
modified: [], // 수정된 파일
|
|
108
|
+
deleted: [], // 삭제된 파일
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// 추가/수정 감지
|
|
112
|
+
for (const [key, file] of Object.entries(currentFiles)) {
|
|
113
|
+
if (!savedHashes[key]) {
|
|
114
|
+
changes.added.push(key);
|
|
115
|
+
} else if (savedHashes[key] !== file.hash) {
|
|
116
|
+
changes.modified.push(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 삭제 감지
|
|
121
|
+
for (const key of Object.keys(savedHashes)) {
|
|
122
|
+
if (!currentFiles[key]) {
|
|
123
|
+
changes.deleted.push(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
changes,
|
|
129
|
+
currentFiles,
|
|
130
|
+
hasChanges: changes.added.length > 0 || changes.modified.length > 0 || changes.deleted.length > 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* _Policy 파일 내용에서 변수 치환 (협업자 로컬화)
|
|
136
|
+
* @param {string} content - 원본 파일 내용
|
|
137
|
+
* @param {object} config - 협업자 설정
|
|
138
|
+
* @returns {string} - 로컬화된 내용
|
|
139
|
+
*/
|
|
140
|
+
function localizeContent(content, config) {
|
|
141
|
+
if (!config.isCoworker) {
|
|
142
|
+
return content; // 오너는 그대로
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const coworkerFolder = config.coworkerFolder || '';
|
|
146
|
+
const coworkerFolderName = `yy_Coworker_${coworkerFolder}`;
|
|
147
|
+
|
|
148
|
+
// 변수 치환
|
|
149
|
+
let localized = content;
|
|
150
|
+
localized = localized.replace(/\{COWORKER_NAME\}/g, coworkerFolder);
|
|
151
|
+
localized = localized.replace(/\{COWORKER_FOLDER\}/g, coworkerFolderName);
|
|
152
|
+
localized = localized.replace(/\{BRANCH_NAME\}/g, `coworker/${coworkerFolder}`);
|
|
153
|
+
|
|
154
|
+
return localized;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* .claude/rules/local/ 폴더에 로컬화된 정책 생성
|
|
159
|
+
* _Policy/ 폴더 구조를 그대로 미러링 (서브파일 구조)
|
|
160
|
+
*/
|
|
161
|
+
function updateLocalRules(localPath, currentFiles, changes, config) {
|
|
162
|
+
const localRulesPath = path.join(localPath, '.claude', 'rules', 'local');
|
|
163
|
+
const allDocuPath = path.join(localPath, 'yy_All_Docu');
|
|
164
|
+
|
|
165
|
+
// 폴더 생성
|
|
166
|
+
if (!fs.existsSync(localRulesPath)) {
|
|
167
|
+
fs.mkdirSync(localRulesPath, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isCoworker = config?.isCoworker || false;
|
|
171
|
+
const coworkerFolder = config?.coworkerFolder || '';
|
|
172
|
+
const projectName = config?.projectName || 'Unknown Project';
|
|
173
|
+
|
|
174
|
+
// _Policy 파일들 분류 (번호순 정렬)
|
|
175
|
+
const policyFiles = Object.entries(currentFiles)
|
|
176
|
+
.filter(([key]) => key.startsWith('_Policy/'))
|
|
177
|
+
.map(([key, file]) => ({ key, ...file }))
|
|
178
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
179
|
+
|
|
180
|
+
// _Infra_Config 파일들 분류
|
|
181
|
+
const infraConfigFiles = Object.entries(currentFiles)
|
|
182
|
+
.filter(([key]) => key.startsWith('_Infra_Config/'))
|
|
183
|
+
.map(([key, file]) => ({ key, ...file }));
|
|
184
|
+
|
|
185
|
+
const createdFiles = [];
|
|
186
|
+
|
|
187
|
+
// ========================================
|
|
188
|
+
// 1. _Policy 파일들을 서브파일 구조로 복사
|
|
189
|
+
// ========================================
|
|
190
|
+
for (const file of policyFiles) {
|
|
191
|
+
// _Policy/00_project_overview.md → 00_project_overview.md
|
|
192
|
+
const relativePath = file.key.replace('_Policy/', '');
|
|
193
|
+
const targetPath = path.join(localRulesPath, relativePath);
|
|
194
|
+
|
|
195
|
+
// 디렉토리 생성
|
|
196
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
197
|
+
|
|
198
|
+
// 파일 읽기 및 로컬화
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(file.fullPath, 'utf-8');
|
|
201
|
+
const localizedContent = localizeContent(content, config);
|
|
202
|
+
fs.writeFileSync(targetPath, localizedContent, 'utf-8');
|
|
203
|
+
createdFiles.push(relativePath);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(`[KingCast] 정책 파일 복사 실패: ${file.key} - ${e.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ========================================
|
|
210
|
+
// 2. 협업자용 설정 파일 생성 (_coworker_config.md)
|
|
211
|
+
// ========================================
|
|
212
|
+
if (isCoworker) {
|
|
213
|
+
const coworkerConfigContent = `# 협업자 설정 (킹캐스트 자동 생성)
|
|
214
|
+
|
|
215
|
+
> 이 파일은 킹캐스트가 로컬에서 생성합니다.
|
|
216
|
+
> 서버에서 내려오는 파일이 아니므로 Pull해도 덮어씌워지지 않습니다.
|
|
217
|
+
> 킹캐스트 재실행 시 갱신됩니다.
|
|
218
|
+
|
|
219
|
+
## 프로젝트 정보
|
|
220
|
+
- 프로젝트: ${projectName}
|
|
221
|
+
- 역할: 협업자
|
|
222
|
+
- 협업자 폴더명: ${coworkerFolder}
|
|
223
|
+
- 작업 폴더: yy_Coworker_${coworkerFolder}/
|
|
224
|
+
- 권장 브랜치: coworker/${coworkerFolder}
|
|
225
|
+
- 최종 동기화: ${new Date().toISOString()}
|
|
226
|
+
|
|
227
|
+
## DocuKing 문서 작업 범위
|
|
228
|
+
|
|
229
|
+
※ 아래는 DocuKing 동기화 대상 폴더(yy_All_Docu/, yy_Coworker_*/, zz_ai_*/)에만 해당
|
|
230
|
+
※ 코드 파일(src/, backend/, frontend/ 등)은 자유롭게 수정 가능
|
|
231
|
+
|
|
232
|
+
### 킹푸시 가능한 영역
|
|
233
|
+
- \`yy_Coworker_${coworkerFolder}/\` 폴더 안의 모든 파일
|
|
234
|
+
|
|
235
|
+
### 읽기만 가능한 영역
|
|
236
|
+
- \`yy_All_Docu/\` - 오너의 문서
|
|
237
|
+
- \`yy_Coworker_*/\` - 다른 협업자의 문서
|
|
238
|
+
- \`zz_ai_*/\` - 오너의 AI 기록
|
|
239
|
+
|
|
240
|
+
### 주의: 위 폴더들은 수정해도 Pull하면 원본으로 덮어씌워짐
|
|
241
|
+
- 수정이 필요하면 자기 폴더에 사본을 만들거나 오너에게 연락하세요
|
|
242
|
+
|
|
243
|
+
## Git과 킹푸시는 별개
|
|
244
|
+
|
|
245
|
+
### Git 커밋/푸시 (소스 보호)
|
|
246
|
+
- 단위 작업 완료 시 AI가 "커밋/푸시할까요?" 제안
|
|
247
|
+
- 코드 변경사항 백업 목적
|
|
248
|
+
|
|
249
|
+
### 킹푸시 (문서 동기화)
|
|
250
|
+
- Plan 완료(docuking_done) 시 자동으로 킹푸시 실행
|
|
251
|
+
- DocuKing 문서 동기화 목적
|
|
252
|
+
|
|
253
|
+
## Git 브랜치 가이드
|
|
254
|
+
|
|
255
|
+
### 권장 브랜치
|
|
256
|
+
\`\`\`bash
|
|
257
|
+
git checkout -b coworker/${coworkerFolder}
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
### 절대 금지
|
|
261
|
+
- \`git push origin main\` 실행 금지
|
|
262
|
+
- \`git checkout main\`에서 작업 금지
|
|
263
|
+
- 강제 푸시(\`--force\`) 금지
|
|
264
|
+
|
|
265
|
+
## 정책 모순 발견 시
|
|
266
|
+
정책 문서에 모순이나 문제가 있다면:
|
|
267
|
+
1. 자기 폴더에 발견 내용 기록
|
|
268
|
+
2. 오너에게 직접 연락 (카톡, 이메일 등)
|
|
269
|
+
3. 오너가 정책 수정 후 Push하면 다음 Pull에서 반영됨
|
|
270
|
+
|
|
271
|
+
## 환경 정보 (읽기 전용 참고)
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
// _Infra_Config에서 민감하지 않은 정보 추가
|
|
275
|
+
let infraInfo = '';
|
|
276
|
+
for (const file of infraConfigFiles) {
|
|
277
|
+
const fileName = path.basename(file.key);
|
|
278
|
+
// 배포 관련 정보만 참조 (민감 정보 제외)
|
|
279
|
+
if (fileName.includes('deploy') || fileName.includes('README')) {
|
|
280
|
+
infraInfo += `\n참고 문서: yy_All_Docu/${file.key}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (infraInfo) {
|
|
285
|
+
const fullCoworkerConfig = coworkerConfigContent + infraInfo;
|
|
286
|
+
fs.writeFileSync(path.join(localRulesPath, '_coworker_config.md'), fullCoworkerConfig, 'utf-8');
|
|
287
|
+
} else {
|
|
288
|
+
fs.writeFileSync(path.join(localRulesPath, '_coworker_config.md'), coworkerConfigContent + '\n(환경 정보 없음)\n', 'utf-8');
|
|
289
|
+
}
|
|
290
|
+
createdFiles.push('_coworker_config.md');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ========================================
|
|
294
|
+
// 3. 정책 목록 인덱스 파일 생성 (00_index.md)
|
|
295
|
+
// ========================================
|
|
296
|
+
let indexContent = `# 정책 문서 인덱스 (킹캐스트 자동 생성)
|
|
297
|
+
|
|
298
|
+
> 이 파일은 DocuKing 킹캐스트에 의해 자동 생성되었습니다.
|
|
299
|
+
> 수동 수정하지 마세요. Pull 시 덮어씌워집니다.
|
|
300
|
+
|
|
301
|
+
## 프로젝트 정보
|
|
302
|
+
- 프로젝트: ${projectName}
|
|
303
|
+
- 역할: ${isCoworker ? `협업자 (${coworkerFolder})` : '오너'}
|
|
304
|
+
- 최종 동기화: ${new Date().toISOString()}
|
|
305
|
+
|
|
306
|
+
## AI 필독
|
|
307
|
+
|
|
308
|
+
**아래 정책 문서들을 반드시 읽고 내용을 숙지하세요.**
|
|
309
|
+
목록만 보지 말고, 각 파일의 내용을 읽어서 이해하세요.
|
|
310
|
+
정책 문서의 규칙을 따라 작업해야 합니다.
|
|
311
|
+
|
|
312
|
+
## 정책 문서 목록
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
if (policyFiles.length === 0) {
|
|
316
|
+
indexContent += '\n(정책 문서가 없습니다)\n';
|
|
317
|
+
} else {
|
|
318
|
+
for (const file of policyFiles) {
|
|
319
|
+
const relativePath = file.key.replace('_Policy/', '');
|
|
320
|
+
indexContent += `- [${relativePath}](./${relativePath})\n`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (isCoworker) {
|
|
325
|
+
indexContent += `\n## 협업자 설정\n- [_coworker_config.md](./_coworker_config.md)\n`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fs.writeFileSync(path.join(localRulesPath, '00_index.md'), indexContent, 'utf-8');
|
|
329
|
+
createdFiles.push('00_index.md');
|
|
330
|
+
|
|
331
|
+
// ========================================
|
|
332
|
+
// 4. 삭제된 정책에 해당하는 로컬 파일 삭제
|
|
333
|
+
// ========================================
|
|
334
|
+
if (changes.deleted.length > 0) {
|
|
335
|
+
for (const deletedKey of changes.deleted) {
|
|
336
|
+
if (deletedKey.startsWith('_Policy/')) {
|
|
337
|
+
const relativePath = deletedKey.replace('_Policy/', '');
|
|
338
|
+
const targetPath = path.join(localRulesPath, relativePath);
|
|
339
|
+
if (fs.existsSync(targetPath)) {
|
|
340
|
+
try {
|
|
341
|
+
fs.unlinkSync(targetPath);
|
|
342
|
+
console.error(`[KingCast] 삭제된 정책 파일 제거: ${relativePath}`);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error(`[KingCast] 파일 삭제 실패: ${relativePath} - ${e.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
localRulesPath,
|
|
353
|
+
createdFiles,
|
|
354
|
+
policyCount: policyFiles.length,
|
|
355
|
+
infraConfigCount: infraConfigFiles.length,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* .gitignore에 킹캐스트 민감 파일만 추가
|
|
361
|
+
*
|
|
362
|
+
* Git 제외 대상 (민감 정보):
|
|
363
|
+
* - .docuking/config.json: API 키 포함
|
|
364
|
+
* - _coworker_config.md: 협업자별 로컬 설정
|
|
365
|
+
*
|
|
366
|
+
* Git 포함 대상 (백업):
|
|
367
|
+
* - .claude/rules/local/*.md: 정책 파일 (폴더구조, API규칙 등)
|
|
368
|
+
*/
|
|
369
|
+
function updateGitignoreForKingcast(localPath) {
|
|
370
|
+
const gitignorePath = path.join(localPath, '.gitignore');
|
|
371
|
+
|
|
372
|
+
let content = '';
|
|
373
|
+
if (fs.existsSync(gitignorePath)) {
|
|
374
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let changed = false;
|
|
378
|
+
|
|
379
|
+
// 이전 버전에서 .claude/rules/local/ 전체 제외했던 경우 → 수정
|
|
380
|
+
if (content.includes('.claude/rules/local/') && !content.includes('.claude/rules/local/_coworker_config.md')) {
|
|
381
|
+
content = content.replace('.claude/rules/local/', '.claude/rules/local/_coworker_config.md');
|
|
382
|
+
changed = true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// _coworker_config.md가 없으면 추가
|
|
386
|
+
if (!content.includes('.claude/rules/local/_coworker_config.md')) {
|
|
387
|
+
content += '\n# DocuKing 킹캐스트 민감 파일 (Git 제외)\n';
|
|
388
|
+
content += '.claude/rules/local/_coworker_config.md\n';
|
|
389
|
+
changed = true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (changed) {
|
|
393
|
+
fs.writeFileSync(gitignorePath, content, 'utf-8');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return changed;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 킹캐스트 실행 (Pull 시 호출)
|
|
401
|
+
* @returns 변경 사항 요약
|
|
402
|
+
*/
|
|
403
|
+
export async function executeKingcast(localPath) {
|
|
404
|
+
const config = getLocalConfig(localPath);
|
|
405
|
+
|
|
406
|
+
if (!config) {
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
message: 'DocuKing이 초기화되지 않았습니다. docuking_init을 먼저 실행하세요.',
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 변경 감지
|
|
414
|
+
const { changes, currentFiles, hasChanges } = detectChanges(localPath);
|
|
415
|
+
|
|
416
|
+
if (!hasChanges) {
|
|
417
|
+
return {
|
|
418
|
+
success: true,
|
|
419
|
+
hasChanges: false,
|
|
420
|
+
message: '정책/환경 변경 없음',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 로컬 룰 업데이트 (서브파일 구조)
|
|
425
|
+
const result = updateLocalRules(localPath, currentFiles, changes, config);
|
|
426
|
+
|
|
427
|
+
// 해시 저장 (다음 비교용)
|
|
428
|
+
const newHashes = {};
|
|
429
|
+
for (const [key, file] of Object.entries(currentFiles)) {
|
|
430
|
+
newHashes[key] = file.hash;
|
|
431
|
+
}
|
|
432
|
+
saveKingcastHashes(localPath, newHashes);
|
|
433
|
+
|
|
434
|
+
// .gitignore 업데이트
|
|
435
|
+
updateGitignoreForKingcast(localPath);
|
|
436
|
+
|
|
437
|
+
// 변경 요약 메시지 생성
|
|
438
|
+
const summary = [];
|
|
439
|
+
if (changes.added.length > 0) {
|
|
440
|
+
summary.push(`추가: ${changes.added.length}개`);
|
|
441
|
+
}
|
|
442
|
+
if (changes.modified.length > 0) {
|
|
443
|
+
summary.push(`수정: ${changes.modified.length}개`);
|
|
444
|
+
}
|
|
445
|
+
if (changes.deleted.length > 0) {
|
|
446
|
+
summary.push(`삭제: ${changes.deleted.length}개`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 킹캐스트 결과 메시지
|
|
450
|
+
const isCoworker = config?.isCoworker || false;
|
|
451
|
+
let message = `📢 킹캐스트: 정책/환경 변경 감지 (${summary.join(', ')})`;
|
|
452
|
+
|
|
453
|
+
if (result.policyCount > 0) {
|
|
454
|
+
message += `\n 📄 정책 파일 ${result.policyCount}개 → .claude/rules/local/`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 정책 변경 시 경고 메시지 추가
|
|
458
|
+
if (changes.modified.length > 0) {
|
|
459
|
+
message += `\n\n⚠️ 중요: 정책이 변경되었습니다!`;
|
|
460
|
+
message += `\n 기존 코드가 새 정책과 맞지 않을 수 있습니다.`;
|
|
461
|
+
message += `\n 변경된 정책 파일을 읽고, 기존 코드에도 새 정책을 적용하세요.`;
|
|
462
|
+
message += `\n 적용하지 않으면 코드가 일관성을 잃고 스파게티가 됩니다.`;
|
|
463
|
+
message += `\n 리팩토링이 필요하더라도 반드시 수행하세요.`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
hasChanges: true,
|
|
469
|
+
changes,
|
|
470
|
+
summary: summary.join(', '),
|
|
471
|
+
message,
|
|
472
|
+
details: {
|
|
473
|
+
added: changes.added,
|
|
474
|
+
modified: changes.modified,
|
|
475
|
+
deleted: changes.deleted,
|
|
476
|
+
localRulesPath: result.localRulesPath,
|
|
477
|
+
createdFiles: result.createdFiles,
|
|
478
|
+
policyCount: result.policyCount,
|
|
479
|
+
infraConfigCount: result.infraConfigCount,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 킹캐스트 상태 확인
|
|
486
|
+
*/
|
|
487
|
+
export async function handleKingcastStatus(args) {
|
|
488
|
+
const localPath = args.localPath || process.cwd();
|
|
489
|
+
const config = getLocalConfig(localPath);
|
|
490
|
+
|
|
491
|
+
if (!config) {
|
|
492
|
+
return {
|
|
493
|
+
content: [
|
|
494
|
+
{
|
|
495
|
+
type: 'text',
|
|
496
|
+
text: '오류: DocuKing이 초기화되지 않았습니다.',
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const { changes, currentFiles, hasChanges } = detectChanges(localPath);
|
|
503
|
+
const savedHashes = loadKingcastHashes(localPath);
|
|
504
|
+
|
|
505
|
+
let statusText = `# 킹캐스트 상태
|
|
506
|
+
|
|
507
|
+
## 프로젝트 정보
|
|
508
|
+
- 프로젝트: ${config.projectName || 'Unknown'}
|
|
509
|
+
- 역할: ${config.isCoworker ? `협업자 (${config.coworkerFolder})` : '오너'}
|
|
510
|
+
|
|
511
|
+
## 킹캐스트 대상 파일
|
|
512
|
+
`;
|
|
513
|
+
|
|
514
|
+
// _Infra_Config 파일
|
|
515
|
+
statusText += '\n### _Infra_Config/\n';
|
|
516
|
+
const infraConfigFiles = Object.keys(currentFiles).filter(k => k.startsWith('_Infra_Config/'));
|
|
517
|
+
if (infraConfigFiles.length === 0) {
|
|
518
|
+
statusText += '(없음)\n';
|
|
519
|
+
} else {
|
|
520
|
+
for (const key of infraConfigFiles) {
|
|
521
|
+
statusText += `- ${key}\n`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// _Policy 파일
|
|
526
|
+
statusText += '\n### _Policy/\n';
|
|
527
|
+
const policyFiles = Object.keys(currentFiles).filter(k => k.startsWith('_Policy/'));
|
|
528
|
+
if (policyFiles.length === 0) {
|
|
529
|
+
statusText += '(없음)\n';
|
|
530
|
+
} else {
|
|
531
|
+
for (const key of policyFiles) {
|
|
532
|
+
statusText += `- ${key}\n`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 변경 사항
|
|
537
|
+
statusText += '\n## 변경 사항\n';
|
|
538
|
+
if (!hasChanges) {
|
|
539
|
+
statusText += '변경 없음 (동기화 완료)\n';
|
|
540
|
+
} else {
|
|
541
|
+
if (changes.added.length > 0) {
|
|
542
|
+
statusText += `\n### 추가됨 (${changes.added.length}개)\n`;
|
|
543
|
+
for (const key of changes.added) {
|
|
544
|
+
statusText += `- ${key}\n`;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (changes.modified.length > 0) {
|
|
548
|
+
statusText += `\n### 수정됨 (${changes.modified.length}개)\n`;
|
|
549
|
+
for (const key of changes.modified) {
|
|
550
|
+
statusText += `- ${key}\n`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (changes.deleted.length > 0) {
|
|
554
|
+
statusText += `\n### 삭제됨 (${changes.deleted.length}개)\n`;
|
|
555
|
+
for (const key of changes.deleted) {
|
|
556
|
+
statusText += `- ${key}\n`;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
content: [
|
|
563
|
+
{
|
|
564
|
+
type: 'text',
|
|
565
|
+
text: statusText,
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
}
|