@mandujs/core 0.9.46 → 0.10.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/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +8 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/filling/filling.ts +88 -6
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +2 -0
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu 설정 Diff 유틸리티 📊
|
|
3
|
+
*
|
|
4
|
+
* ont-run의 differ 기법을 참고하여 구현
|
|
5
|
+
* @see DNA/ont-run/src/lockfile/differ.ts
|
|
6
|
+
*
|
|
7
|
+
* 특징:
|
|
8
|
+
* - 설정 객체 간 변경사항 감지
|
|
9
|
+
* - 민감 정보 자동 마스킹 (redact)
|
|
10
|
+
* - 콘솔 친화적 시각화
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ANSI 색상 코드 (외부 의존성 없음)
|
|
14
|
+
const ansi = {
|
|
15
|
+
reset: "\x1b[0m",
|
|
16
|
+
bold: "\x1b[1m",
|
|
17
|
+
dim: "\x1b[2m",
|
|
18
|
+
cyan: "\x1b[36m",
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
red: "\x1b[31m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const pc = {
|
|
25
|
+
cyan: (s: string) => `${ansi.cyan}${s}${ansi.reset}`,
|
|
26
|
+
green: (s: string) => `${ansi.green}${s}${ansi.reset}`,
|
|
27
|
+
red: (s: string) => `${ansi.red}${s}${ansi.reset}`,
|
|
28
|
+
yellow: (s: string) => `${ansi.yellow}${s}${ansi.reset}`,
|
|
29
|
+
bold: (s: string) => `${ansi.bold}${s}${ansi.reset}`,
|
|
30
|
+
dim: (s: string) => `${ansi.dim}${s}${ansi.reset}`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// 타입 정의
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
export interface ConfigDiff {
|
|
38
|
+
/** 변경사항 존재 여부 */
|
|
39
|
+
hasChanges: boolean;
|
|
40
|
+
/** 비교 시각 */
|
|
41
|
+
timestamp: string;
|
|
42
|
+
|
|
43
|
+
/** MCP 서버 변경 */
|
|
44
|
+
mcpServers: {
|
|
45
|
+
added: string[];
|
|
46
|
+
removed: string[];
|
|
47
|
+
modified: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
changes: Record<string, { old: unknown; new: unknown }>;
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** 프로젝트 설정 변경 */
|
|
54
|
+
projectConfig: {
|
|
55
|
+
added: string[];
|
|
56
|
+
removed: string[];
|
|
57
|
+
modified: Array<{
|
|
58
|
+
key: string;
|
|
59
|
+
old: unknown;
|
|
60
|
+
new: unknown;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface DiffFormatOptions {
|
|
66
|
+
/** 색상 사용 여부 (기본값: true) */
|
|
67
|
+
color?: boolean;
|
|
68
|
+
/** 상세 출력 (기본값: false) */
|
|
69
|
+
verbose?: boolean;
|
|
70
|
+
/** 마스킹할 키 목록 */
|
|
71
|
+
redactKeys?: string[];
|
|
72
|
+
/** 비밀 정보 출력 허용 (기본값: false) */
|
|
73
|
+
showSecrets?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 기본 민감 키 목록 */
|
|
77
|
+
const DEFAULT_REDACT_KEYS = [
|
|
78
|
+
"token",
|
|
79
|
+
"secret",
|
|
80
|
+
"key",
|
|
81
|
+
"password",
|
|
82
|
+
"authorization",
|
|
83
|
+
"cookie",
|
|
84
|
+
"apikey",
|
|
85
|
+
"api_key",
|
|
86
|
+
"access_token",
|
|
87
|
+
"refresh_token",
|
|
88
|
+
"private_key",
|
|
89
|
+
"credential",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// ============================================
|
|
93
|
+
// Diff 계산
|
|
94
|
+
// ============================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 두 설정 객체 비교
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const oldConfig = { port: 3000, mcpServers: { a: { url: "..." } } };
|
|
102
|
+
* const newConfig = { port: 3001, mcpServers: { b: { url: "..." } } };
|
|
103
|
+
* const diff = diffConfig(oldConfig, newConfig);
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function diffConfig(
|
|
107
|
+
oldConfig: Record<string, unknown>,
|
|
108
|
+
newConfig: Record<string, unknown>
|
|
109
|
+
): ConfigDiff {
|
|
110
|
+
const timestamp = new Date().toISOString();
|
|
111
|
+
|
|
112
|
+
// MCP 서버 비교
|
|
113
|
+
const oldMcp = (oldConfig.mcpServers ?? {}) as Record<string, unknown>;
|
|
114
|
+
const newMcp = (newConfig.mcpServers ?? {}) as Record<string, unknown>;
|
|
115
|
+
const mcpDiff = diffObjects(oldMcp, newMcp, "mcpServers");
|
|
116
|
+
|
|
117
|
+
// 프로젝트 설정 비교 (mcpServers 제외)
|
|
118
|
+
const { mcpServers: _o, ...oldRest } = oldConfig;
|
|
119
|
+
const { mcpServers: _n, ...newRest } = newConfig;
|
|
120
|
+
const projectDiff = diffFlatConfig(oldRest, newRest);
|
|
121
|
+
|
|
122
|
+
const hasChanges =
|
|
123
|
+
mcpDiff.added.length > 0 ||
|
|
124
|
+
mcpDiff.removed.length > 0 ||
|
|
125
|
+
mcpDiff.modified.length > 0 ||
|
|
126
|
+
projectDiff.added.length > 0 ||
|
|
127
|
+
projectDiff.removed.length > 0 ||
|
|
128
|
+
projectDiff.modified.length > 0;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
hasChanges,
|
|
132
|
+
timestamp,
|
|
133
|
+
mcpServers: mcpDiff,
|
|
134
|
+
projectConfig: projectDiff,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 객체 간 diff (MCP 서버 등 중첩 객체용)
|
|
140
|
+
*/
|
|
141
|
+
function diffObjects(
|
|
142
|
+
oldObj: Record<string, unknown>,
|
|
143
|
+
newObj: Record<string, unknown>,
|
|
144
|
+
_context: string
|
|
145
|
+
): ConfigDiff["mcpServers"] {
|
|
146
|
+
const oldKeys = new Set(Object.keys(oldObj));
|
|
147
|
+
const newKeys = new Set(Object.keys(newObj));
|
|
148
|
+
|
|
149
|
+
const added: string[] = [];
|
|
150
|
+
const removed: string[] = [];
|
|
151
|
+
const modified: Array<{ name: string; changes: Record<string, { old: unknown; new: unknown }> }> = [];
|
|
152
|
+
|
|
153
|
+
// 추가된 키
|
|
154
|
+
for (const key of newKeys) {
|
|
155
|
+
if (!oldKeys.has(key)) {
|
|
156
|
+
added.push(key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 삭제된 키
|
|
161
|
+
for (const key of oldKeys) {
|
|
162
|
+
if (!newKeys.has(key)) {
|
|
163
|
+
removed.push(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 수정된 키
|
|
168
|
+
for (const key of oldKeys) {
|
|
169
|
+
if (newKeys.has(key)) {
|
|
170
|
+
const changes = findChanges(
|
|
171
|
+
oldObj[key] as Record<string, unknown>,
|
|
172
|
+
newObj[key] as Record<string, unknown>
|
|
173
|
+
);
|
|
174
|
+
if (Object.keys(changes).length > 0) {
|
|
175
|
+
modified.push({ name: key, changes });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { added, removed, modified };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 플랫 설정 비교 (최상위 키-값)
|
|
185
|
+
*/
|
|
186
|
+
function diffFlatConfig(
|
|
187
|
+
oldConfig: Record<string, unknown>,
|
|
188
|
+
newConfig: Record<string, unknown>
|
|
189
|
+
): ConfigDiff["projectConfig"] {
|
|
190
|
+
const oldKeys = new Set(Object.keys(oldConfig));
|
|
191
|
+
const newKeys = new Set(Object.keys(newConfig));
|
|
192
|
+
|
|
193
|
+
const added: string[] = [];
|
|
194
|
+
const removed: string[] = [];
|
|
195
|
+
const modified: Array<{ key: string; old: unknown; new: unknown }> = [];
|
|
196
|
+
|
|
197
|
+
// 추가된 키
|
|
198
|
+
for (const key of newKeys) {
|
|
199
|
+
if (!oldKeys.has(key)) {
|
|
200
|
+
added.push(key);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 삭제된 키
|
|
205
|
+
for (const key of oldKeys) {
|
|
206
|
+
if (!newKeys.has(key)) {
|
|
207
|
+
removed.push(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 수정된 키
|
|
212
|
+
for (const key of oldKeys) {
|
|
213
|
+
if (newKeys.has(key)) {
|
|
214
|
+
if (!deepEqual(oldConfig[key], newConfig[key])) {
|
|
215
|
+
modified.push({
|
|
216
|
+
key,
|
|
217
|
+
old: oldConfig[key],
|
|
218
|
+
new: newConfig[key],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { added, removed, modified };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 두 객체 간 변경된 필드 찾기
|
|
229
|
+
*/
|
|
230
|
+
function findChanges(
|
|
231
|
+
oldObj: Record<string, unknown> | undefined,
|
|
232
|
+
newObj: Record<string, unknown> | undefined
|
|
233
|
+
): Record<string, { old: unknown; new: unknown }> {
|
|
234
|
+
const changes: Record<string, { old: unknown; new: unknown }> = {};
|
|
235
|
+
|
|
236
|
+
if (!oldObj || !newObj) {
|
|
237
|
+
return changes;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
|
241
|
+
|
|
242
|
+
for (const key of allKeys) {
|
|
243
|
+
const oldVal = oldObj[key];
|
|
244
|
+
const newVal = newObj[key];
|
|
245
|
+
|
|
246
|
+
if (!deepEqual(oldVal, newVal)) {
|
|
247
|
+
changes[key] = { old: oldVal, new: newVal };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return changes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 깊은 비교
|
|
256
|
+
*/
|
|
257
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
258
|
+
if (a === b) return true;
|
|
259
|
+
if (a === null || b === null) return a === b;
|
|
260
|
+
if (typeof a !== typeof b) return false;
|
|
261
|
+
|
|
262
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
263
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
264
|
+
if (a.length !== b.length) return false;
|
|
265
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
269
|
+
|
|
270
|
+
const keysA = Object.keys(a as object);
|
|
271
|
+
const keysB = Object.keys(b as object);
|
|
272
|
+
|
|
273
|
+
if (keysA.length !== keysB.length) return false;
|
|
274
|
+
|
|
275
|
+
return keysA.every((key) =>
|
|
276
|
+
deepEqual(
|
|
277
|
+
(a as Record<string, unknown>)[key],
|
|
278
|
+
(b as Record<string, unknown>)[key]
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================
|
|
287
|
+
// Diff 포맷팅
|
|
288
|
+
// ============================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Diff를 문자열로 포맷
|
|
292
|
+
*/
|
|
293
|
+
export function formatConfigDiff(
|
|
294
|
+
diff: ConfigDiff,
|
|
295
|
+
options: DiffFormatOptions = {}
|
|
296
|
+
): string {
|
|
297
|
+
const {
|
|
298
|
+
color = true,
|
|
299
|
+
verbose = false,
|
|
300
|
+
redactKeys = DEFAULT_REDACT_KEYS,
|
|
301
|
+
showSecrets = false,
|
|
302
|
+
} = options;
|
|
303
|
+
|
|
304
|
+
const c = color ? pc : noColor;
|
|
305
|
+
const lines: string[] = [];
|
|
306
|
+
|
|
307
|
+
// 헤더
|
|
308
|
+
lines.push(c.cyan("╭─────────────────────────────────────────────────╮"));
|
|
309
|
+
lines.push(c.cyan("│") + " " + c.bold("mandu.config 변경 감지") + " " + c.cyan("│"));
|
|
310
|
+
lines.push(c.cyan("├─────────────────────────────────────────────────┤"));
|
|
311
|
+
|
|
312
|
+
if (!diff.hasChanges) {
|
|
313
|
+
lines.push(c.cyan("│") + " " + c.green("✓ 변경사항 없음") + " " + c.cyan("│"));
|
|
314
|
+
lines.push(c.cyan("╰─────────────────────────────────────────────────╯"));
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lines.push(c.cyan("│") + " " + c.cyan("│"));
|
|
319
|
+
|
|
320
|
+
// MCP 서버 변경
|
|
321
|
+
if (
|
|
322
|
+
diff.mcpServers.added.length > 0 ||
|
|
323
|
+
diff.mcpServers.removed.length > 0 ||
|
|
324
|
+
diff.mcpServers.modified.length > 0
|
|
325
|
+
) {
|
|
326
|
+
lines.push(c.cyan("│") + " " + c.bold("MCP 서버:") + " " + c.cyan("│"));
|
|
327
|
+
|
|
328
|
+
for (const name of diff.mcpServers.added) {
|
|
329
|
+
lines.push(c.cyan("│") + " " + c.green(`+ ${name}`) + " (추가됨)" + padding(30 - name.length) + c.cyan("│"));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const name of diff.mcpServers.removed) {
|
|
333
|
+
lines.push(c.cyan("│") + " " + c.red(`- ${name}`) + " (삭제됨)" + padding(30 - name.length) + c.cyan("│"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const mod of diff.mcpServers.modified) {
|
|
337
|
+
lines.push(c.cyan("│") + " " + c.yellow(`~ ${mod.name}`) + " (수정됨)" + padding(30 - mod.name.length) + c.cyan("│"));
|
|
338
|
+
|
|
339
|
+
if (verbose) {
|
|
340
|
+
for (const [key, val] of Object.entries(mod.changes)) {
|
|
341
|
+
const oldStr = redactValue(key, val.old, redactKeys, showSecrets);
|
|
342
|
+
const newStr = redactValue(key, val.new, redactKeys, showSecrets);
|
|
343
|
+
lines.push(c.cyan("│") + ` ${c.dim(key)}: ${c.red(oldStr)} → ${c.green(newStr)}` + padding(15) + c.cyan("│"));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
lines.push(c.cyan("│") + " " + c.cyan("│"));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 프로젝트 설정 변경
|
|
352
|
+
if (
|
|
353
|
+
diff.projectConfig.added.length > 0 ||
|
|
354
|
+
diff.projectConfig.removed.length > 0 ||
|
|
355
|
+
diff.projectConfig.modified.length > 0
|
|
356
|
+
) {
|
|
357
|
+
lines.push(c.cyan("│") + " " + c.bold("프로젝트 설정:") + " " + c.cyan("│"));
|
|
358
|
+
|
|
359
|
+
for (const key of diff.projectConfig.added) {
|
|
360
|
+
lines.push(c.cyan("│") + " " + c.green(`+ ${key}`) + " (추가됨)" + padding(30 - key.length) + c.cyan("│"));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const key of diff.projectConfig.removed) {
|
|
364
|
+
lines.push(c.cyan("│") + " " + c.red(`- ${key}`) + " (삭제됨)" + padding(30 - key.length) + c.cyan("│"));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const mod of diff.projectConfig.modified) {
|
|
368
|
+
const oldStr = redactValue(mod.key, mod.old, redactKeys, showSecrets);
|
|
369
|
+
const newStr = redactValue(mod.key, mod.new, redactKeys, showSecrets);
|
|
370
|
+
lines.push(c.cyan("│") + " " + c.yellow(`~ ${mod.key}:`) + ` ${c.red(oldStr)} → ${c.green(newStr)}` + padding(10) + c.cyan("│"));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
lines.push(c.cyan("│") + " " + c.cyan("│"));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
lines.push(c.cyan("╰─────────────────────────────────────────────────╯"));
|
|
377
|
+
|
|
378
|
+
return lines.join("\n");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Diff를 콘솔에 출력
|
|
383
|
+
*/
|
|
384
|
+
export function printConfigDiff(
|
|
385
|
+
diff: ConfigDiff,
|
|
386
|
+
options: DiffFormatOptions = {}
|
|
387
|
+
): void {
|
|
388
|
+
console.log(formatConfigDiff(diff, options));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================
|
|
392
|
+
// 유틸리티
|
|
393
|
+
// ============================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 민감 정보 마스킹
|
|
397
|
+
*/
|
|
398
|
+
function redactValue(
|
|
399
|
+
key: string,
|
|
400
|
+
value: unknown,
|
|
401
|
+
redactKeys: string[],
|
|
402
|
+
showSecrets: boolean
|
|
403
|
+
): string {
|
|
404
|
+
const strValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
405
|
+
|
|
406
|
+
if (showSecrets) {
|
|
407
|
+
return strValue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const lowerKey = key.toLowerCase();
|
|
411
|
+
const shouldRedact = redactKeys.some((rk) => lowerKey.includes(rk.toLowerCase()));
|
|
412
|
+
|
|
413
|
+
if (shouldRedact) {
|
|
414
|
+
return "***";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return strValue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 패딩 생성 (고정 폭 출력용)
|
|
422
|
+
*/
|
|
423
|
+
function padding(n: number): string {
|
|
424
|
+
return " ".repeat(Math.max(0, n));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 색상 없는 출력용 더미 함수들
|
|
429
|
+
*/
|
|
430
|
+
const noColor = {
|
|
431
|
+
cyan: (s: string) => s,
|
|
432
|
+
green: (s: string) => s,
|
|
433
|
+
red: (s: string) => s,
|
|
434
|
+
yellow: (s: string) => s,
|
|
435
|
+
bold: (s: string) => s,
|
|
436
|
+
dim: (s: string) => s,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// ============================================
|
|
440
|
+
// 요약 함수
|
|
441
|
+
// ============================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Diff 요약 정보
|
|
445
|
+
*/
|
|
446
|
+
export function summarizeDiff(diff: ConfigDiff): string {
|
|
447
|
+
if (!diff.hasChanges) {
|
|
448
|
+
return "변경사항 없음";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const parts: string[] = [];
|
|
452
|
+
|
|
453
|
+
const mcpTotal =
|
|
454
|
+
diff.mcpServers.added.length +
|
|
455
|
+
diff.mcpServers.removed.length +
|
|
456
|
+
diff.mcpServers.modified.length;
|
|
457
|
+
|
|
458
|
+
const configTotal =
|
|
459
|
+
diff.projectConfig.added.length +
|
|
460
|
+
diff.projectConfig.removed.length +
|
|
461
|
+
diff.projectConfig.modified.length;
|
|
462
|
+
|
|
463
|
+
if (mcpTotal > 0) {
|
|
464
|
+
parts.push(`MCP 서버: ${mcpTotal}개 변경`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (configTotal > 0) {
|
|
468
|
+
parts.push(`설정: ${configTotal}개 변경`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return parts.join(", ");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 변경사항이 있는지 빠르게 확인
|
|
476
|
+
*/
|
|
477
|
+
export function hasConfigChanges(
|
|
478
|
+
oldConfig: Record<string, unknown>,
|
|
479
|
+
newConfig: Record<string, unknown>
|
|
480
|
+
): boolean {
|
|
481
|
+
return diffConfig(oldConfig, newConfig).hasChanges;
|
|
482
|
+
}
|