docuking-mcp 3.1.0 → 3.6.1
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/README.md +4 -4
- package/handlers/docs.js +229 -16
- package/handlers/kingcast.js +6 -6
- package/handlers/sync.js +165 -57
- package/index.js +239 -139
- package/lib/config.js +4 -2
- package/lib/index-cache.js +393 -17
- package/lib/init.js +23 -21
- package/lib/utils.js +1 -1
- package/package.json +1 -1
package/handlers/sync.js
CHANGED
|
@@ -46,6 +46,12 @@ import {
|
|
|
46
46
|
updateIndexAfterSync,
|
|
47
47
|
computeFileHash,
|
|
48
48
|
getIndexStats,
|
|
49
|
+
// v2 추가
|
|
50
|
+
markAsDeleted,
|
|
51
|
+
wasDeletedLocally,
|
|
52
|
+
clearDeletedEntry,
|
|
53
|
+
classifyForPush,
|
|
54
|
+
classifyForPull,
|
|
49
55
|
} from '../lib/index-cache.js';
|
|
50
56
|
|
|
51
57
|
// 레포지토리 매핑 (메모리에 캐시)
|
|
@@ -74,6 +80,18 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
|
|
|
74
80
|
// Co-worker 권한은 API Key 형식에서 판단
|
|
75
81
|
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
76
82
|
|
|
83
|
+
// 기존 config 확인 - 프로젝트 ID가 변경되면 인덱스 초기화
|
|
84
|
+
const existingConfig = getLocalConfig(localPath);
|
|
85
|
+
if (existingConfig && existingConfig.projectId && existingConfig.projectId !== projectId) {
|
|
86
|
+
// 프로젝트 ID가 변경됨 - 인덱스 파일 삭제
|
|
87
|
+
const indexPath = path.join(localPath, '.docuking', 'index.json');
|
|
88
|
+
if (fs.existsSync(indexPath)) {
|
|
89
|
+
fs.unlinkSync(indexPath);
|
|
90
|
+
console.error(`[DocuKing] 프로젝트 ID 변경 감지: ${existingConfig.projectId} → ${projectId}`);
|
|
91
|
+
console.error(`[DocuKing] 기존 인덱스 초기화됨`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
77
95
|
// .docuking/config.json에 설정 저장
|
|
78
96
|
saveLocalConfig(localPath, {
|
|
79
97
|
projectId,
|
|
@@ -84,7 +102,7 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
|
|
|
84
102
|
createdAt: new Date().toISOString(),
|
|
85
103
|
});
|
|
86
104
|
|
|
87
|
-
// 폴더 생성: 오너는 yy_All_Docu/, 협업자는
|
|
105
|
+
// 폴더 생성: 오너는 yy_All_Docu/, 협업자는 zz_Coworker_{폴더명}/ (별도)
|
|
88
106
|
const mainFolderName = 'yy_All_Docu';
|
|
89
107
|
const mainFolderPath = path.join(localPath, mainFolderName);
|
|
90
108
|
|
|
@@ -96,9 +114,10 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
|
|
|
96
114
|
let coworkerFolderName = null;
|
|
97
115
|
let coworkerFolderPath = null;
|
|
98
116
|
|
|
99
|
-
// AI 폴더 (오너: xy_TalkTodoPlan, 협업자:
|
|
117
|
+
// AI 폴더 (오너: xy_TalkTodoPlan, 협업자: {폴더명}_TalkTodoPlan)
|
|
100
118
|
const ownerAiFolder = 'xy_TalkTodoPlan';
|
|
101
|
-
|
|
119
|
+
// 협업자 AI 폴더는 동적으로 생성 (coworkerFolder가 있을 때만)
|
|
120
|
+
const coworkerAiFolder = coworkerFolder ? `${coworkerFolder}_TalkTodoPlan` : null;
|
|
102
121
|
|
|
103
122
|
// xx_ 시스템 폴더 목록 (오너 전용)
|
|
104
123
|
const systemFolders = ['xx_Infra_Config', 'xx_Policy'];
|
|
@@ -110,8 +129,8 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
|
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
if (isCoworker) {
|
|
113
|
-
// 협업자:
|
|
114
|
-
coworkerFolderName = `
|
|
132
|
+
// 협업자: zz_Coworker_{폴더명}/ 폴더를 yy_All_Docu/ 밖에 별도 생성
|
|
133
|
+
coworkerFolderName = `zz_Coworker_${coworkerFolder}`;
|
|
115
134
|
coworkerFolderPath = path.join(localPath, coworkerFolderName);
|
|
116
135
|
if (!fs.existsSync(coworkerFolderPath)) {
|
|
117
136
|
fs.mkdirSync(coworkerFolderPath, { recursive: true });
|
|
@@ -121,7 +140,7 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
|
|
|
121
140
|
if (!fs.existsSync(coworkerPrivatePath)) {
|
|
122
141
|
fs.mkdirSync(coworkerPrivatePath, { recursive: true });
|
|
123
142
|
}
|
|
124
|
-
// 협업자 폴더 안에
|
|
143
|
+
// 협업자 폴더 안에 {폴더명}_TalkTodoPlan 폴더 생성
|
|
125
144
|
const coworkerAiFolderPath = path.join(coworkerFolderPath, coworkerAiFolder);
|
|
126
145
|
if (!fs.existsSync(coworkerAiFolderPath)) {
|
|
127
146
|
fs.mkdirSync(coworkerAiFolderPath, { recursive: true });
|
|
@@ -244,11 +263,11 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
|
|
|
244
263
|
|
|
245
264
|
// Co-worker 권한은 API Key 형식에서 판단
|
|
246
265
|
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
247
|
-
const coworkerFolderName = isCoworker ? `
|
|
266
|
+
const coworkerFolderName = isCoworker ? `zz_Coworker_${coworkerFolder}` : null;
|
|
248
267
|
|
|
249
268
|
// 작업 폴더 결정: 프로젝트 루트 기준 절대경로 사용
|
|
250
|
-
// 오너:
|
|
251
|
-
// 협업자:
|
|
269
|
+
// 오너: xx_*/ + xy_*/ + yy_All_Docu/ 폴더들 Push
|
|
270
|
+
// 협업자: zz_Coworker_{폴더명}/ 폴더만 Push
|
|
252
271
|
const mainFolderPath = path.join(localPath, 'yy_All_Docu');
|
|
253
272
|
|
|
254
273
|
// Push 대상 폴더 목록 (프로젝트 루트 기준)
|
|
@@ -258,7 +277,7 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
|
|
|
258
277
|
console.error(`[DocuKing] Push 권한: isCoworker=${isCoworker}, coworkerFolder=${coworkerFolder}, coworkerFolderName=${coworkerFolderName}`);
|
|
259
278
|
|
|
260
279
|
if (isCoworker) {
|
|
261
|
-
// 협업자:
|
|
280
|
+
// 협업자: zz_Coworker_{폴더명}/ 폴더 + xx_Urgent/ 폴더 Push
|
|
262
281
|
const coworkerPath = path.join(localPath, coworkerFolderName);
|
|
263
282
|
console.error(`[DocuKing] 협업자 Push 대상 폴더: ${coworkerPath}`);
|
|
264
283
|
if (!fs.existsSync(coworkerPath)) {
|
|
@@ -294,13 +313,17 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
294
313
|
};
|
|
295
314
|
}
|
|
296
315
|
|
|
297
|
-
// 오너 Push 대상: xx_*/ +
|
|
316
|
+
// 오너 Push 대상: xx_*/ + xy_*/ + yy_All_Docu/ 폴더들
|
|
298
317
|
pushTargetFolders.push({ localPath: mainFolderPath, serverPrefix: 'yy_All_Docu' });
|
|
299
318
|
|
|
300
|
-
//
|
|
319
|
+
// 특수폴더 패턴으로 Push 대상 폴더 찾기
|
|
320
|
+
// 특수폴더: xx_*, xy_TalkTodoPlan, yy_All_Docu, zz_Coworker_*
|
|
301
321
|
const rootEntries = fs.readdirSync(localPath, { withFileTypes: true });
|
|
302
322
|
for (const entry of rootEntries) {
|
|
303
|
-
if (entry.isDirectory() && (
|
|
323
|
+
if (entry.isDirectory() && (
|
|
324
|
+
entry.name.startsWith('xx_') ||
|
|
325
|
+
entry.name === 'xy_TalkTodoPlan'
|
|
326
|
+
)) {
|
|
304
327
|
const folderPath = path.join(localPath, entry.name);
|
|
305
328
|
pushTargetFolders.push({ localPath: folderPath, serverPrefix: entry.name });
|
|
306
329
|
}
|
|
@@ -704,7 +727,7 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
704
727
|
},
|
|
705
728
|
body: JSON.stringify({
|
|
706
729
|
projectId,
|
|
707
|
-
path: file.serverPath, // 서버 경로 (코워커는
|
|
730
|
+
path: file.serverPath, // 서버 경로 (코워커는 zz_Coworker_{폴더명}/파일경로)
|
|
708
731
|
content,
|
|
709
732
|
encoding, // 'utf-8' 또는 'base64'
|
|
710
733
|
message, // 커밋 메시지
|
|
@@ -783,16 +806,18 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
783
806
|
}
|
|
784
807
|
|
|
785
808
|
// ========================================
|
|
786
|
-
// 4. 인덱스 기반 삭제 전파 (2026-01-24
|
|
809
|
+
// 4. 인덱스 기반 삭제 전파 (v2: 2026-01-24 강화)
|
|
787
810
|
// ========================================
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
// - 인덱스에
|
|
791
|
-
// - 인덱스에
|
|
811
|
+
// 3-way 비교:
|
|
812
|
+
// - 인덱스에 있음 + 로컬에 없음 + 서버에 있음 = 로컬 삭제 → 서버도 삭제
|
|
813
|
+
// - 인덱스에 있음 + 로컬에 없음 + 서버 수정됨 = 충돌 (로컬 삭제 vs 서버 수정)
|
|
814
|
+
// - 인덱스에 없음 + 로컬에 없음 + 서버에 있음 = 다른 곳에서 생성 → 무시
|
|
815
|
+
// - source: 'web' 파일은 삭제 보호
|
|
792
816
|
// ========================================
|
|
793
817
|
let deleted = 0;
|
|
794
818
|
const deletedFilePaths = [];
|
|
795
819
|
let protectedFiles = [];
|
|
820
|
+
const conflicts = [];
|
|
796
821
|
|
|
797
822
|
// 인덱스에서 삭제된 파일 찾기 (인덱스에 있었는데 로컬에 없는 파일)
|
|
798
823
|
const locallyDeletedFiles = [];
|
|
@@ -814,8 +839,39 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
814
839
|
|
|
815
840
|
// 로컬에 파일이 없으면 = 삭제된 파일
|
|
816
841
|
if (!fs.existsSync(fullPath)) {
|
|
842
|
+
const serverMeta = serverPathToMeta[indexPath];
|
|
843
|
+
const indexEntry = localIndex.files[indexPath];
|
|
844
|
+
|
|
845
|
+
// 서버에 없으면 이미 삭제됨 → 스킵
|
|
846
|
+
if (!serverMeta) {
|
|
847
|
+
console.error(`[DocuKing] 이미 삭제됨: ${indexPath}`);
|
|
848
|
+
// deletedFiles 기록 정리
|
|
849
|
+
clearDeletedEntry(localIndex, indexPath);
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// source: 'web' 파일은 삭제 보호
|
|
854
|
+
if (serverMeta.source === 'web') {
|
|
855
|
+
console.error(`[DocuKing] 삭제 보호 (source:web): ${indexPath}`);
|
|
856
|
+
protectedFiles.push(indexPath);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// 서버가 수정되었는지 확인 (충돌 감지)
|
|
861
|
+
const serverModified = serverMeta.hash !== (indexEntry.serverHash || indexEntry.hash);
|
|
862
|
+
if (serverModified) {
|
|
863
|
+
console.error(`[DocuKing] ⚠️ 충돌: ${indexPath} (로컬 삭제 vs 서버 수정)`);
|
|
864
|
+
conflicts.push({
|
|
865
|
+
path: indexPath,
|
|
866
|
+
type: 'delete-vs-modify',
|
|
867
|
+
localAction: 'delete',
|
|
868
|
+
serverHash: serverMeta.hash,
|
|
869
|
+
});
|
|
870
|
+
// 충돌 시에도 로컬 우선 정책으로 삭제 진행 (경고만 출력)
|
|
871
|
+
}
|
|
872
|
+
|
|
817
873
|
locallyDeletedFiles.push(indexPath);
|
|
818
|
-
console.error(`[DocuKing] 삭제 감지: ${indexPath}
|
|
874
|
+
console.error(`[DocuKing] 삭제 감지: ${indexPath}`);
|
|
819
875
|
}
|
|
820
876
|
}
|
|
821
877
|
|
|
@@ -839,11 +895,11 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
839
895
|
const deleteResult = await deleteResponse.json();
|
|
840
896
|
deleted = deleteResult.deleted || 0;
|
|
841
897
|
deletedFilePaths.push(...(deleteResult.deletedPaths || []));
|
|
842
|
-
protectedFiles = deleteResult.protected || [];
|
|
843
898
|
|
|
844
|
-
//
|
|
899
|
+
// 인덱스에서 삭제 처리 (v2: markAsDeleted 사용)
|
|
845
900
|
for (const deletedPath of locallyDeletedFiles) {
|
|
846
|
-
|
|
901
|
+
const entry = localIndex.files[deletedPath];
|
|
902
|
+
markAsDeleted(localIndex, deletedPath, entry?.hash);
|
|
847
903
|
}
|
|
848
904
|
|
|
849
905
|
console.error(`[DocuKing] 서버 삭제 완료: ${deleted}개`);
|
|
@@ -858,6 +914,15 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
858
914
|
}
|
|
859
915
|
}
|
|
860
916
|
|
|
917
|
+
// 충돌 경고 출력
|
|
918
|
+
if (conflicts.length > 0) {
|
|
919
|
+
console.error(`\n[DocuKing] ⚠️ ${conflicts.length}개 충돌 발생 (로컬 우선 정책으로 처리됨):`);
|
|
920
|
+
for (const c of conflicts) {
|
|
921
|
+
console.error(` - ${c.path}: ${c.type}`);
|
|
922
|
+
}
|
|
923
|
+
console.error('');
|
|
924
|
+
}
|
|
925
|
+
|
|
861
926
|
// 5. 빈 폴더 생성
|
|
862
927
|
let createdEmptyFolders = 0;
|
|
863
928
|
if (emptyFolders.length > 0) {
|
|
@@ -888,12 +953,13 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
888
953
|
}
|
|
889
954
|
|
|
890
955
|
// ========================================
|
|
891
|
-
// 로컬 인덱스 업데이트 및 저장
|
|
956
|
+
// 로컬 인덱스 업데이트 및 저장 (v2)
|
|
892
957
|
// ========================================
|
|
893
|
-
if (syncedFiles.length > 0) {
|
|
894
|
-
|
|
958
|
+
if (syncedFiles.length > 0 || deletedFilePaths.length > 0) {
|
|
959
|
+
// syncType: 'push'로 lastPush 타임스탬프 업데이트
|
|
960
|
+
updateIndexAfterSync(localIndex, syncedFiles, deletedFilePaths, { syncType: 'push' });
|
|
895
961
|
if (saveIndex(localPath, localIndex)) {
|
|
896
|
-
console.error(`[DocuKing] 로컬 인덱스 업데이트: ${syncedFiles.length}개
|
|
962
|
+
console.error(`[DocuKing] 로컬 인덱스 업데이트: ${syncedFiles.length}개 동기화, ${deletedFilePaths.length}개 삭제`);
|
|
897
963
|
}
|
|
898
964
|
}
|
|
899
965
|
|
|
@@ -1033,8 +1099,8 @@ export async function handlePullInternal(args) {
|
|
|
1033
1099
|
// 오너 전용 폴더
|
|
1034
1100
|
const ownerFolders = ['xx_Infra_Config', 'xx_Policy', 'xy_TalkTodoPlan'];
|
|
1035
1101
|
|
|
1036
|
-
// 협업자 전용 폴더 (
|
|
1037
|
-
|
|
1102
|
+
// 협업자 전용 폴더 (zz_Coworker_{폴더명}/ 안에)
|
|
1103
|
+
// AI 폴더는 {폴더명}_TalkTodoPlan 형식 (coworkerFolder 기반 동적 생성)
|
|
1038
1104
|
|
|
1039
1105
|
// 공통 폴더 체크
|
|
1040
1106
|
for (const folder of commonFolders) {
|
|
@@ -1047,8 +1113,9 @@ export async function handlePullInternal(args) {
|
|
|
1047
1113
|
}
|
|
1048
1114
|
|
|
1049
1115
|
if (isCoworker) {
|
|
1050
|
-
// 협업자:
|
|
1051
|
-
const coworkerFolderName = `
|
|
1116
|
+
// 협업자: zz_Coworker_{폴더명}/ 안에 하위 폴더 체크
|
|
1117
|
+
const coworkerFolderName = `zz_Coworker_${coworkerFolder}`;
|
|
1118
|
+
const coworkerSubFolders = [`${coworkerFolder}_TalkTodoPlan`, '_Private'];
|
|
1052
1119
|
const coworkerBasePath = path.join(localPath, coworkerFolderName);
|
|
1053
1120
|
|
|
1054
1121
|
// 협업자 루트 폴더
|
|
@@ -1197,10 +1264,9 @@ export async function handlePullInternal(args) {
|
|
|
1197
1264
|
const serverMeta = serverPathToMeta[folder.path];
|
|
1198
1265
|
let fullPath;
|
|
1199
1266
|
if (folder.path.startsWith('xx_') ||
|
|
1200
|
-
folder.path.startsWith('
|
|
1267
|
+
folder.path.startsWith('xy_TalkTodoPlan') ||
|
|
1201
1268
|
folder.path.startsWith('yy_All_Docu') ||
|
|
1202
|
-
folder.path.startsWith('
|
|
1203
|
-
folder.path.startsWith('zz_ai_')) {
|
|
1269
|
+
folder.path.startsWith('zz_Coworker_')) {
|
|
1204
1270
|
fullPath = path.join(localPath, folder.path);
|
|
1205
1271
|
} else {
|
|
1206
1272
|
fullPath = path.join(mainFolderPath, folder.path);
|
|
@@ -1230,10 +1296,9 @@ export async function handlePullInternal(args) {
|
|
|
1230
1296
|
for (const file of files_only) {
|
|
1231
1297
|
let fullPath;
|
|
1232
1298
|
if (file.path.startsWith('xx_') ||
|
|
1233
|
-
file.path.startsWith('
|
|
1299
|
+
file.path.startsWith('xy_TalkTodoPlan') ||
|
|
1234
1300
|
file.path.startsWith('yy_All_Docu') ||
|
|
1235
|
-
file.path.startsWith('
|
|
1236
|
-
file.path.startsWith('zz_ai_')) {
|
|
1301
|
+
file.path.startsWith('zz_Coworker_')) {
|
|
1237
1302
|
fullPath = path.join(localPath, file.path);
|
|
1238
1303
|
} else {
|
|
1239
1304
|
fullPath = path.join(mainFolderPath, file.path);
|
|
@@ -1276,17 +1341,46 @@ export async function handlePullInternal(args) {
|
|
|
1276
1341
|
}
|
|
1277
1342
|
} else {
|
|
1278
1343
|
// ========================================
|
|
1279
|
-
// 로컬에 파일이 없는 경우
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1344
|
+
// 로컬에 파일이 없는 경우 (v2 좀비 방지 강화)
|
|
1345
|
+
// 1. files에 있음 = 로컬에서 삭제한 파일 → markAsDeleted 후 스킵
|
|
1346
|
+
// 2. deletedFiles에 있음 = 이미 삭제 표시됨 → 서버 변경 확인 후 처리
|
|
1347
|
+
// 3. 둘 다 없음 = 서버에서 새로 생성된 파일 → 다운로드
|
|
1282
1348
|
// ========================================
|
|
1349
|
+
|
|
1350
|
+
// Case 1: files에 아직 있음 (첫 Pull 시점)
|
|
1283
1351
|
if (localIndex.files && localIndex.files[file.path]) {
|
|
1284
|
-
console.error(`[DocuKing] 로컬 삭제
|
|
1352
|
+
console.error(`[DocuKing] 로컬 삭제 감지: ${file.path} → deletedFiles에 기록`);
|
|
1353
|
+
// deletedFiles에 기록
|
|
1354
|
+
const entry = localIndex.files[file.path];
|
|
1355
|
+
markAsDeleted(localIndex, file.path, entry.hash);
|
|
1285
1356
|
results.push({ type: 'skip', path: file.path, reason: 'locally-deleted' });
|
|
1286
1357
|
skipped++;
|
|
1287
1358
|
current++;
|
|
1288
1359
|
continue;
|
|
1289
1360
|
}
|
|
1361
|
+
|
|
1362
|
+
// Case 2: deletedFiles에 있음 (이전에 삭제 표시됨)
|
|
1363
|
+
const deleted = wasDeletedLocally(localIndex, file.path);
|
|
1364
|
+
if (deleted.isDeleted) {
|
|
1365
|
+
// 서버가 수정되었는지 확인
|
|
1366
|
+
if (serverHash === deleted.entry.lastHash) {
|
|
1367
|
+
// 서버 변경 없음 → 스킵 (좀비 방지)
|
|
1368
|
+
console.error(`[DocuKing] 좀비 방지: ${file.path} (로컬 삭제 유지)`);
|
|
1369
|
+
results.push({ type: 'skip', path: file.path, reason: 'zombie-prevention' });
|
|
1370
|
+
} else {
|
|
1371
|
+
// 서버가 수정됨 → 충돌! (로컬 삭제 vs 서버 수정)
|
|
1372
|
+
console.error(`[DocuKing] ⚠️ 충돌: ${file.path} (로컬 삭제 vs 서버 수정) → 서버 버전 다운로드`);
|
|
1373
|
+
// 충돌 시 서버 버전 다운로드 (사용자가 결정할 수 있도록)
|
|
1374
|
+
filesToDownload.push({ ...file, fullPath, conflict: true });
|
|
1375
|
+
current++;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
skipped++;
|
|
1379
|
+
current++;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Case 3: 둘 다 없음 = 새 파일
|
|
1290
1384
|
}
|
|
1291
1385
|
|
|
1292
1386
|
// 인덱스에 없고 로컬에 없음 = 서버에서 새로 생성된 파일 → 다운로드
|
|
@@ -1437,12 +1531,17 @@ export async function handlePullInternal(args) {
|
|
|
1437
1531
|
}
|
|
1438
1532
|
|
|
1439
1533
|
// ========================================
|
|
1440
|
-
// 로컬 인덱스 업데이트 및 저장
|
|
1534
|
+
// 로컬 인덱스 업데이트 및 저장 (v2)
|
|
1441
1535
|
// ========================================
|
|
1536
|
+
// Pull 중 발견된 삭제 파일들도 인덱스에 기록됨 (markAsDeleted 호출됨)
|
|
1537
|
+
// 따라서 syncedFiles가 0이어도 인덱스가 변경되었을 수 있음
|
|
1538
|
+
if (saveIndex(localPath, localIndex)) {
|
|
1539
|
+
console.error(`[DocuKing] 로컬 인덱스 업데이트 (lastPull 갱신)`);
|
|
1540
|
+
}
|
|
1442
1541
|
if (syncedFiles.length > 0) {
|
|
1443
|
-
updateIndexAfterSync(localIndex, syncedFiles, []);
|
|
1542
|
+
updateIndexAfterSync(localIndex, syncedFiles, [], { syncType: 'pull' });
|
|
1444
1543
|
if (saveIndex(localPath, localIndex)) {
|
|
1445
|
-
console.error(`[DocuKing] 로컬
|
|
1544
|
+
console.error(`[DocuKing] 로컬 인덱스: ${syncedFiles.length}개 파일 동기화됨`);
|
|
1446
1545
|
}
|
|
1447
1546
|
}
|
|
1448
1547
|
|
|
@@ -1524,9 +1623,9 @@ export async function handlePullInternal(args) {
|
|
|
1524
1623
|
// 서버 경로에 따라 로컬 경로 결정
|
|
1525
1624
|
let fullPath;
|
|
1526
1625
|
if (deletedFile.path.startsWith('xx_') ||
|
|
1527
|
-
deletedFile.path.startsWith('
|
|
1528
|
-
deletedFile.path.startsWith('
|
|
1529
|
-
deletedFile.path.startsWith('
|
|
1626
|
+
deletedFile.path.startsWith('xy_TalkTodoPlan') ||
|
|
1627
|
+
deletedFile.path.startsWith('yy_All_Docu') ||
|
|
1628
|
+
deletedFile.path.startsWith('zz_Coworker_')) {
|
|
1530
1629
|
fullPath = path.join(localPath, deletedFile.path);
|
|
1531
1630
|
} else {
|
|
1532
1631
|
fullPath = path.join(mainFolderPath, deletedFile.path);
|
|
@@ -1550,6 +1649,10 @@ export async function handlePullInternal(args) {
|
|
|
1550
1649
|
deletedLocallyPaths.push(deletedFile.path);
|
|
1551
1650
|
console.error(`[DocuKing] 로컬 삭제 (서버에서 삭제됨): ${deletedFile.path}`);
|
|
1552
1651
|
|
|
1652
|
+
// 인덱스에서도 제거 (v2)
|
|
1653
|
+
removeIndexEntry(localIndex, deletedFile.path);
|
|
1654
|
+
clearDeletedEntry(localIndex, deletedFile.path);
|
|
1655
|
+
|
|
1553
1656
|
// 빈 폴더 정리
|
|
1554
1657
|
const parentDir = path.dirname(fullPath);
|
|
1555
1658
|
try {
|
|
@@ -1563,6 +1666,8 @@ export async function handlePullInternal(args) {
|
|
|
1563
1666
|
}
|
|
1564
1667
|
} else {
|
|
1565
1668
|
console.error(`[DocuKing] 로컬 유지 (로컬 수정이 최신): ${deletedFile.path}`);
|
|
1669
|
+
// 다음 Push에서 부활할 것이므로 deletedFiles에서 제거
|
|
1670
|
+
clearDeletedEntry(localIndex, deletedFile.path);
|
|
1566
1671
|
}
|
|
1567
1672
|
} catch (e) {
|
|
1568
1673
|
console.error(`[DocuKing] 삭제 동기화 실패: ${deletedFile.path} - ${e.message}`);
|
|
@@ -1814,7 +1919,7 @@ export async function handleStatus(args) {
|
|
|
1814
1919
|
|
|
1815
1920
|
// Co-worker 권한은 API Key 형식에서 판단
|
|
1816
1921
|
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
1817
|
-
const coworkerFolderName = isCoworker ? `
|
|
1922
|
+
const coworkerFolderName = isCoworker ? `zz_Coworker_${coworkerFolder}` : null;
|
|
1818
1923
|
|
|
1819
1924
|
// 권한 정보 구성
|
|
1820
1925
|
let permissionInfo = '';
|
|
@@ -1856,21 +1961,24 @@ export async function handleStatus(args) {
|
|
|
1856
1961
|
const mainFolderPath = path.join(localPath, 'yy_All_Docu');
|
|
1857
1962
|
|
|
1858
1963
|
if (isCoworker) {
|
|
1859
|
-
// 협업자:
|
|
1964
|
+
// 협업자: zz_Coworker_{폴더명}/ 폴더에서 파일 수집
|
|
1860
1965
|
const coworkerPath = path.join(localPath, coworkerFolderName);
|
|
1861
1966
|
if (fs.existsSync(coworkerPath)) {
|
|
1862
1967
|
collectFilesSimple(coworkerPath, '', localFiles);
|
|
1863
1968
|
}
|
|
1864
1969
|
pushableFiles = localFiles; // 협업자는 자기 폴더의 모든 파일 Push 가능
|
|
1865
1970
|
} else {
|
|
1866
|
-
// 오너: xx_*/ +
|
|
1971
|
+
// 오너: xx_*/ + xy_*/ + yy_All_Docu/ 폴더에서 파일 수집
|
|
1867
1972
|
if (fs.existsSync(mainFolderPath)) {
|
|
1868
1973
|
collectFilesSimple(mainFolderPath, '', localFiles);
|
|
1869
1974
|
}
|
|
1870
|
-
// xx_*,
|
|
1975
|
+
// 특수폴더들도 수집: xx_*, xy_TalkTodoPlan
|
|
1871
1976
|
const rootEntries = fs.readdirSync(localPath, { withFileTypes: true });
|
|
1872
1977
|
for (const entry of rootEntries) {
|
|
1873
|
-
if (entry.isDirectory() && (
|
|
1978
|
+
if (entry.isDirectory() && (
|
|
1979
|
+
entry.name.startsWith('xx_') ||
|
|
1980
|
+
entry.name === 'xy_TalkTodoPlan'
|
|
1981
|
+
)) {
|
|
1874
1982
|
const folderPath = path.join(localPath, entry.name);
|
|
1875
1983
|
collectFilesSimple(folderPath, '', localFiles);
|
|
1876
1984
|
}
|
|
@@ -1948,7 +2056,7 @@ export async function handleDelete(args) {
|
|
|
1948
2056
|
|
|
1949
2057
|
// 협업자는 자기 폴더만 삭제 가능
|
|
1950
2058
|
if (isCoworker) {
|
|
1951
|
-
const coworkerPrefix = `
|
|
2059
|
+
const coworkerPrefix = `zz_Coworker_${coworkerFolder}/`;
|
|
1952
2060
|
const invalidPaths = paths.filter(p => !p.startsWith(coworkerPrefix) && !p.startsWith('xx_Urgent/'));
|
|
1953
2061
|
if (invalidPaths.length > 0) {
|
|
1954
2062
|
return {
|
|
@@ -1963,7 +2071,7 @@ export async function handleDelete(args) {
|
|
|
1963
2071
|
|
|
1964
2072
|
// 오너도 협업자 폴더는 삭제 금지
|
|
1965
2073
|
if (!isCoworker) {
|
|
1966
|
-
const coworkerPaths = paths.filter(p => p.startsWith('
|
|
2074
|
+
const coworkerPaths = paths.filter(p => p.startsWith('zz_Coworker_'));
|
|
1967
2075
|
if (coworkerPaths.length > 0 && !force) {
|
|
1968
2076
|
return {
|
|
1969
2077
|
content: [{
|
|
@@ -2039,9 +2147,9 @@ export async function handleDelete(args) {
|
|
|
2039
2147
|
try {
|
|
2040
2148
|
let localFilePath;
|
|
2041
2149
|
if (serverPath.startsWith('xx_') ||
|
|
2042
|
-
serverPath.startsWith('
|
|
2043
|
-
serverPath.startsWith('
|
|
2044
|
-
serverPath.startsWith('
|
|
2150
|
+
serverPath.startsWith('xy_TalkTodoPlan') ||
|
|
2151
|
+
serverPath.startsWith('yy_All_Docu') ||
|
|
2152
|
+
serverPath.startsWith('zz_Coworker_')) {
|
|
2045
2153
|
localFilePath = path.join(localPath, serverPath);
|
|
2046
2154
|
} else {
|
|
2047
2155
|
localFilePath = path.join(localPath, 'yy_All_Docu', serverPath);
|