@mandujs/core 0.8.3 → 0.9.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/adapters/base.ts +120 -0
- package/src/brain/adapters/index.ts +8 -0
- package/src/brain/adapters/ollama.ts +249 -0
- package/src/brain/brain.ts +324 -0
- package/src/brain/doctor/analyzer.ts +366 -0
- package/src/brain/doctor/index.ts +40 -0
- package/src/brain/doctor/patcher.ts +349 -0
- package/src/brain/doctor/reporter.ts +336 -0
- package/src/brain/index.ts +45 -0
- package/src/brain/memory.ts +154 -0
- package/src/brain/permissions.ts +270 -0
- package/src/brain/types.ts +268 -0
- package/src/contract/contract.test.ts +381 -0
- package/src/contract/integration.test.ts +394 -0
- package/src/contract/validator.ts +113 -8
- package/src/generator/contract-glue.test.ts +211 -0
- package/src/guard/check.ts +51 -1
- package/src/guard/contract-guard.test.ts +303 -0
- package/src/guard/rules.ts +37 -0
- package/src/index.ts +2 -0
- package/src/openapi/openapi.test.ts +277 -0
- package/src/slot/validator.test.ts +203 -0
- package/src/slot/validator.ts +236 -17
- package/src/watcher/index.ts +44 -0
- package/src/watcher/reporter.ts +232 -0
- package/src/watcher/rules.ts +248 -0
- package/src/watcher/watcher.ts +330 -0
package/src/slot/validator.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slot Content Validator
|
|
3
3
|
* 슬롯 파일 내용을 작성 전에 검증하고 문제를 식별합니다.
|
|
4
|
+
*
|
|
5
|
+
* 검증 항목:
|
|
6
|
+
* 1. 필수 import/export 패턴
|
|
7
|
+
* 2. Mandu.filling() 사용 여부
|
|
8
|
+
* 3. export default Mandu.filling() 형태 검증
|
|
9
|
+
* 4. 핸들러 반환 타입 검증 (ctx.ok(), ctx.json() 등)
|
|
10
|
+
* 5. 금지된 모듈 import 검사
|
|
4
11
|
*/
|
|
5
12
|
|
|
6
13
|
export interface SlotValidationIssue {
|
|
@@ -29,13 +36,103 @@ const FORBIDDEN_IMPORTS = [
|
|
|
29
36
|
"node:worker_threads",
|
|
30
37
|
];
|
|
31
38
|
|
|
32
|
-
// 필수 패턴들
|
|
39
|
+
// 필수 패턴들 (더 엄격한 검사)
|
|
33
40
|
const REQUIRED_PATTERNS = {
|
|
34
41
|
manduImport: /import\s+.*\bMandu\b.*from\s+['"]@mandujs\/core['"]/,
|
|
35
42
|
fillingPattern: /Mandu\s*\.\s*filling\s*\(\s*\)/,
|
|
36
43
|
defaultExport: /export\s+default\b/,
|
|
44
|
+
// export default Mandu.filling() 또는 export default 변수명
|
|
45
|
+
exportDefaultFilling: /export\s+default\s+(Mandu\s*\.\s*filling\s*\(\s*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)/,
|
|
37
46
|
};
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 주석을 제거한 코드 반환
|
|
50
|
+
* 한줄 주석(//)과 블록 주석 제거
|
|
51
|
+
*/
|
|
52
|
+
function stripComments(content: string): string {
|
|
53
|
+
// 문자열 내부는 보존하면서 주석만 제거
|
|
54
|
+
let result = "";
|
|
55
|
+
let i = 0;
|
|
56
|
+
let inString: string | null = null;
|
|
57
|
+
let inComment: "line" | "block" | null = null;
|
|
58
|
+
|
|
59
|
+
while (i < content.length) {
|
|
60
|
+
const char = content[i];
|
|
61
|
+
const nextChar = content[i + 1];
|
|
62
|
+
|
|
63
|
+
// 문자열 처리
|
|
64
|
+
if (!inComment) {
|
|
65
|
+
if (!inString && (char === '"' || char === "'" || char === "`")) {
|
|
66
|
+
inString = char;
|
|
67
|
+
result += char;
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (inString && char === inString && content[i - 1] !== "\\") {
|
|
72
|
+
inString = null;
|
|
73
|
+
result += char;
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (inString) {
|
|
78
|
+
result += char;
|
|
79
|
+
i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 주석 시작 감지
|
|
85
|
+
if (!inString && !inComment) {
|
|
86
|
+
if (char === "/" && nextChar === "/") {
|
|
87
|
+
inComment = "line";
|
|
88
|
+
i += 2;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === "/" && nextChar === "*") {
|
|
92
|
+
inComment = "block";
|
|
93
|
+
i += 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 주석 종료 감지
|
|
99
|
+
if (inComment === "line" && char === "\n") {
|
|
100
|
+
inComment = null;
|
|
101
|
+
result += char; // 줄바꿈은 유지
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inComment === "block" && char === "*" && nextChar === "/") {
|
|
106
|
+
inComment = null;
|
|
107
|
+
i += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 주석 내부면 스킵
|
|
112
|
+
if (inComment) {
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result += char;
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 올바른 응답 패턴 (ctx 메서드 호출)
|
|
125
|
+
const VALID_RESPONSE_PATTERNS = [
|
|
126
|
+
/return\s+ctx\s*\.\s*(ok|json|created|noContent|notFound|badRequest|error|html|redirect|stream)\s*\(/,
|
|
127
|
+
/return\s+new\s+Response\s*\(/,
|
|
128
|
+
/return\s+Response\s*\.\s*(json|redirect)\s*\(/,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// 잘못된 반환 패턴 (일반 객체 직접 반환)
|
|
132
|
+
const INVALID_RETURN_PATTERNS = [
|
|
133
|
+
/return\s+\{\s*[^}]*\}\s*;?\s*$/m, // return { ... }; (Response가 아닌 객체)
|
|
134
|
+
];
|
|
135
|
+
|
|
39
136
|
/**
|
|
40
137
|
* 슬롯 내용을 검증합니다.
|
|
41
138
|
*/
|
|
@@ -43,6 +140,9 @@ export function validateSlotContent(content: string): SlotValidationResult {
|
|
|
43
140
|
const issues: SlotValidationIssue[] = [];
|
|
44
141
|
const lines = content.split("\n");
|
|
45
142
|
|
|
143
|
+
// 주석 제거된 코드 (export default 등 패턴 검사용)
|
|
144
|
+
const codeWithoutComments = stripComments(content);
|
|
145
|
+
|
|
46
146
|
// 1. 금지된 import 검사
|
|
47
147
|
for (let i = 0; i < lines.length; i++) {
|
|
48
148
|
const line = lines[i];
|
|
@@ -90,25 +190,54 @@ export function validateSlotContent(content: string): SlotValidationResult {
|
|
|
90
190
|
});
|
|
91
191
|
}
|
|
92
192
|
|
|
93
|
-
// 4. default export 검사
|
|
94
|
-
|
|
193
|
+
// 4. default export 검사 (강화됨) - 주석 제거된 코드에서 검사
|
|
194
|
+
const hasDefaultExport = REQUIRED_PATTERNS.defaultExport.test(codeWithoutComments);
|
|
195
|
+
const hasExportDefaultFilling = REQUIRED_PATTERNS.exportDefaultFilling.test(codeWithoutComments);
|
|
196
|
+
|
|
197
|
+
if (!hasDefaultExport) {
|
|
198
|
+
// Mandu.filling()이 있는데 export default가 없는 경우 - 변수에 할당만 함
|
|
199
|
+
const fillingVarMatch = codeWithoutComments.match(/(?:const|let|var)\s+(\w+)\s*=\s*Mandu\s*\.\s*filling\s*\(\s*\)/);
|
|
200
|
+
if (fillingVarMatch) {
|
|
201
|
+
const varName = fillingVarMatch[1];
|
|
202
|
+
issues.push({
|
|
203
|
+
code: "MISSING_DEFAULT_EXPORT",
|
|
204
|
+
severity: "error",
|
|
205
|
+
message: `default export가 없습니다. '${varName}'가 export 되지 않았습니다`,
|
|
206
|
+
suggestion: `'export default ${varName};' 를 파일 끝에 추가하거나, 'export default Mandu.filling()...' 형태로 작성하세요`,
|
|
207
|
+
autoFixable: true,
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
issues.push({
|
|
211
|
+
code: "MISSING_DEFAULT_EXPORT",
|
|
212
|
+
severity: "error",
|
|
213
|
+
message: "default export가 없습니다",
|
|
214
|
+
suggestion: "export default Mandu.filling()... 형태로 작성하세요",
|
|
215
|
+
autoFixable: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} else if (hasDefaultExport && REQUIRED_PATTERNS.fillingPattern.test(codeWithoutComments) && !hasExportDefaultFilling) {
|
|
219
|
+
// export default는 있지만 Mandu.filling()을 export하지 않는 경우
|
|
95
220
|
issues.push({
|
|
96
|
-
code: "
|
|
97
|
-
severity: "
|
|
98
|
-
message: "default export
|
|
99
|
-
suggestion: "export default Mandu.filling()... 형태로
|
|
100
|
-
autoFixable:
|
|
221
|
+
code: "INVALID_DEFAULT_EXPORT",
|
|
222
|
+
severity: "warning",
|
|
223
|
+
message: "export default가 Mandu.filling()을 직접 export하지 않습니다",
|
|
224
|
+
suggestion: "export default Mandu.filling()... 형태로 작성하거나, 변수명을 export default로 내보내세요",
|
|
225
|
+
autoFixable: false,
|
|
101
226
|
});
|
|
102
227
|
}
|
|
103
228
|
|
|
104
|
-
// 5. 기본 문법 검사 (간단한 체크)
|
|
105
|
-
const syntaxIssues = checkBasicSyntax(
|
|
229
|
+
// 5. 기본 문법 검사 (간단한 체크) - 주석 제거된 코드로 검사
|
|
230
|
+
const syntaxIssues = checkBasicSyntax(codeWithoutComments, codeWithoutComments.split("\n"));
|
|
106
231
|
issues.push(...syntaxIssues);
|
|
107
232
|
|
|
108
|
-
// 6. HTTP 메서드 핸들러 검사
|
|
109
|
-
const methodIssues = checkHttpMethods(content);
|
|
233
|
+
// 6. HTTP 메서드 핸들러 검사 (강화됨)
|
|
234
|
+
const methodIssues = checkHttpMethods(content, lines);
|
|
110
235
|
issues.push(...methodIssues);
|
|
111
236
|
|
|
237
|
+
// 7. 핸들러 반환 타입 검사 (신규)
|
|
238
|
+
const returnIssues = checkHandlerReturns(content, lines);
|
|
239
|
+
issues.push(...returnIssues);
|
|
240
|
+
|
|
112
241
|
return {
|
|
113
242
|
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
114
243
|
issues,
|
|
@@ -185,7 +314,7 @@ function checkBasicSyntax(
|
|
|
185
314
|
/**
|
|
186
315
|
* HTTP 메서드 핸들러 검사
|
|
187
316
|
*/
|
|
188
|
-
function checkHttpMethods(content: string): SlotValidationIssue[] {
|
|
317
|
+
function checkHttpMethods(content: string, lines: string[]): SlotValidationIssue[] {
|
|
189
318
|
const issues: SlotValidationIssue[] = [];
|
|
190
319
|
|
|
191
320
|
// .get(), .post() 등의 핸들러가 있는지 확인
|
|
@@ -204,14 +333,14 @@ function checkHttpMethods(content: string): SlotValidationIssue[] {
|
|
|
204
333
|
}
|
|
205
334
|
|
|
206
335
|
// ctx.ok(), ctx.json() 등 응답 패턴 확인
|
|
207
|
-
const responsePattern = /ctx\s*\.\s*(ok|json|created|noContent|error|html)\s*\(/;
|
|
336
|
+
const responsePattern = /ctx\s*\.\s*(ok|json|created|noContent|notFound|badRequest|error|html|redirect|stream)\s*\(/;
|
|
208
337
|
if (hasMethod && !responsePattern.test(content)) {
|
|
209
338
|
issues.push({
|
|
210
339
|
code: "NO_RESPONSE_PATTERN",
|
|
211
|
-
severity: "
|
|
212
|
-
message: "응답
|
|
340
|
+
severity: "error", // 에러로 승격 (warning → error)
|
|
341
|
+
message: "ctx 응답 메서드가 없습니다 (ctx.ok(), ctx.json() 등)",
|
|
213
342
|
suggestion:
|
|
214
|
-
"핸들러에서 ctx.ok(), ctx.json() 등으로 응답을
|
|
343
|
+
"핸들러에서 ctx.ok(), ctx.json(), ctx.created() 등으로 응답을 반환하세요. 일반 객체 { ... }를 직접 반환하면 안 됩니다.",
|
|
215
344
|
autoFixable: false,
|
|
216
345
|
});
|
|
217
346
|
}
|
|
@@ -219,6 +348,96 @@ function checkHttpMethods(content: string): SlotValidationIssue[] {
|
|
|
219
348
|
return issues;
|
|
220
349
|
}
|
|
221
350
|
|
|
351
|
+
/**
|
|
352
|
+
* 핸들러 반환 타입 검사 (신규)
|
|
353
|
+
* 핸들러가 올바른 Response 객체를 반환하는지 검사
|
|
354
|
+
*/
|
|
355
|
+
function checkHandlerReturns(content: string, lines: string[]): SlotValidationIssue[] {
|
|
356
|
+
const issues: SlotValidationIssue[] = [];
|
|
357
|
+
|
|
358
|
+
// 핸들러 내부에서 일반 객체를 직접 반환하는 패턴 감지
|
|
359
|
+
// 예: return { data: [], status: "ok" };
|
|
360
|
+
const handlerBlockPattern = /\.(get|post|put|patch|delete)\s*\(\s*(?:async\s*)?\(?(?:ctx)?\)?\s*=>\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gs;
|
|
361
|
+
|
|
362
|
+
let match;
|
|
363
|
+
while ((match = handlerBlockPattern.exec(content)) !== null) {
|
|
364
|
+
const method = match[1].toUpperCase();
|
|
365
|
+
const handlerBody = match[2];
|
|
366
|
+
|
|
367
|
+
// 핸들러 본문에서 return 문 찾기
|
|
368
|
+
const returnStatements = handlerBody.match(/return\s+[^;]+;?/g) || [];
|
|
369
|
+
|
|
370
|
+
for (const returnStmt of returnStatements) {
|
|
371
|
+
// ctx.* 또는 new Response 또는 Response.* 패턴 확인
|
|
372
|
+
const isValidReturn =
|
|
373
|
+
/return\s+ctx\s*\./.test(returnStmt) ||
|
|
374
|
+
/return\s+new\s+Response/.test(returnStmt) ||
|
|
375
|
+
/return\s+Response\s*\./.test(returnStmt);
|
|
376
|
+
|
|
377
|
+
// 일반 객체 직접 반환 감지: return { ... }
|
|
378
|
+
const isObjectReturn = /return\s+\{/.test(returnStmt) && !isValidReturn;
|
|
379
|
+
|
|
380
|
+
// 문자열 직접 반환 감지: return "string" 또는 return 'string'
|
|
381
|
+
const isStringReturn = /return\s+['"`]/.test(returnStmt) && !isValidReturn;
|
|
382
|
+
|
|
383
|
+
// throw 문자열 감지 (Error 객체가 아닌 문자열)
|
|
384
|
+
const throwStringPattern = /throw\s+['"`][^'"`]+['"`]/;
|
|
385
|
+
|
|
386
|
+
if (isObjectReturn) {
|
|
387
|
+
// return 문이 있는 라인 번호 찾기
|
|
388
|
+
const lineNum = findLineNumber(lines, returnStmt.trim().substring(0, 30));
|
|
389
|
+
issues.push({
|
|
390
|
+
code: "INVALID_HANDLER_RETURN",
|
|
391
|
+
severity: "error",
|
|
392
|
+
message: `${method} 핸들러가 일반 객체를 직접 반환합니다`,
|
|
393
|
+
line: lineNum,
|
|
394
|
+
suggestion: "return { ... } 대신 return ctx.ok({ ... }) 또는 return ctx.json({ ... }) 을 사용하세요",
|
|
395
|
+
autoFixable: false,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (isStringReturn) {
|
|
400
|
+
const lineNum = findLineNumber(lines, returnStmt.trim().substring(0, 30));
|
|
401
|
+
issues.push({
|
|
402
|
+
code: "INVALID_HANDLER_RETURN",
|
|
403
|
+
severity: "error",
|
|
404
|
+
message: `${method} 핸들러가 문자열을 직접 반환합니다`,
|
|
405
|
+
line: lineNum,
|
|
406
|
+
suggestion: "return 'text' 대신 return ctx.html('text') 또는 return ctx.ok({ message: 'text' }) 를 사용하세요",
|
|
407
|
+
autoFixable: false,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// throw 문자열 검사
|
|
413
|
+
if (/throw\s+['"`][^'"`]+['"`]/.test(handlerBody)) {
|
|
414
|
+
const lineNum = findLineNumber(lines, "throw");
|
|
415
|
+
issues.push({
|
|
416
|
+
code: "INVALID_THROW_PATTERN",
|
|
417
|
+
severity: "warning",
|
|
418
|
+
message: `${method} 핸들러에서 문자열을 직접 throw합니다`,
|
|
419
|
+
line: lineNum,
|
|
420
|
+
suggestion: "throw 'message' 대신 throw new Error('message') 를 사용하세요",
|
|
421
|
+
autoFixable: false,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return issues;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 특정 텍스트가 있는 라인 번호 찾기
|
|
431
|
+
*/
|
|
432
|
+
function findLineNumber(lines: string[], searchText: string): number | undefined {
|
|
433
|
+
for (let i = 0; i < lines.length; i++) {
|
|
434
|
+
if (lines[i].includes(searchText)) {
|
|
435
|
+
return i + 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
222
441
|
/**
|
|
223
442
|
* 에러 요약 생성
|
|
224
443
|
*/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Watcher Module
|
|
3
|
+
*
|
|
4
|
+
* Watch handles error prevention:
|
|
5
|
+
* - File change detection
|
|
6
|
+
* - Architecture rule warnings
|
|
7
|
+
* - No blocking - warnings only
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Rules
|
|
11
|
+
export {
|
|
12
|
+
MVP_RULES,
|
|
13
|
+
getRulesMap,
|
|
14
|
+
matchGlob,
|
|
15
|
+
matchRules,
|
|
16
|
+
checkNamingConvention,
|
|
17
|
+
checkForbiddenImports,
|
|
18
|
+
validateFile,
|
|
19
|
+
getRule,
|
|
20
|
+
getAllRules,
|
|
21
|
+
} from "./rules";
|
|
22
|
+
|
|
23
|
+
// Watcher
|
|
24
|
+
export {
|
|
25
|
+
FileWatcher,
|
|
26
|
+
createWatcher,
|
|
27
|
+
getWatcher,
|
|
28
|
+
startWatcher,
|
|
29
|
+
stopWatcher,
|
|
30
|
+
type WatcherConfig,
|
|
31
|
+
} from "./watcher";
|
|
32
|
+
|
|
33
|
+
// Reporter
|
|
34
|
+
export {
|
|
35
|
+
formatWarning,
|
|
36
|
+
printWarning,
|
|
37
|
+
formatStatus,
|
|
38
|
+
printStatus,
|
|
39
|
+
printWatchStart,
|
|
40
|
+
printWatchStop,
|
|
41
|
+
generateJsonStatus,
|
|
42
|
+
createConsoleHandler,
|
|
43
|
+
createCollectorHandler,
|
|
44
|
+
} from "./reporter";
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Watch Reporter
|
|
3
|
+
*
|
|
4
|
+
* Formats and outputs watch warnings to the console.
|
|
5
|
+
* Warnings only - never blocks operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WatchWarning, WatchStatus, ArchRule } from "../brain/types";
|
|
9
|
+
import { getRule } from "./rules";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ANSI color codes for terminal output
|
|
13
|
+
*/
|
|
14
|
+
const colors = {
|
|
15
|
+
reset: "\x1b[0m",
|
|
16
|
+
bright: "\x1b[1m",
|
|
17
|
+
dim: "\x1b[2m",
|
|
18
|
+
red: "\x1b[31m",
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
yellow: "\x1b[33m",
|
|
21
|
+
blue: "\x1b[34m",
|
|
22
|
+
magenta: "\x1b[35m",
|
|
23
|
+
cyan: "\x1b[36m",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if colors should be used
|
|
28
|
+
*/
|
|
29
|
+
function useColors(): boolean {
|
|
30
|
+
if (process.env.NO_COLOR) return false;
|
|
31
|
+
if (typeof process?.stdout?.isTTY === "boolean") {
|
|
32
|
+
return process.stdout.isTTY;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Apply color if enabled
|
|
39
|
+
*/
|
|
40
|
+
function color(text: string, colorCode: string): string {
|
|
41
|
+
if (!useColors()) return text;
|
|
42
|
+
return `${colorCode}${text}${colors.reset}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format a warning for terminal output
|
|
47
|
+
*/
|
|
48
|
+
export function formatWarning(warning: WatchWarning): string {
|
|
49
|
+
const rule = getRule(warning.ruleId);
|
|
50
|
+
|
|
51
|
+
// Event icons
|
|
52
|
+
const eventIcons: Record<string, string> = {
|
|
53
|
+
create: "📝",
|
|
54
|
+
modify: "✏️",
|
|
55
|
+
delete: "🗑️",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const eventIcon = eventIcons[warning.event] || "📄";
|
|
59
|
+
const ruleIcon = rule?.action === "error" ? "❌" : "⚠️";
|
|
60
|
+
const ruleColor = rule?.action === "error" ? colors.red : colors.yellow;
|
|
61
|
+
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
|
|
64
|
+
lines.push(
|
|
65
|
+
`${eventIcon} ${ruleIcon} ${color(`[${warning.ruleId}]`, ruleColor)} ${warning.file}`
|
|
66
|
+
);
|
|
67
|
+
lines.push(` ${warning.message}`);
|
|
68
|
+
|
|
69
|
+
if (rule?.description) {
|
|
70
|
+
lines.push(` ${color("💡", colors.cyan)} ${rule.description}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Print a warning to console
|
|
78
|
+
*/
|
|
79
|
+
export function printWarning(warning: WatchWarning): void {
|
|
80
|
+
console.log(formatWarning(warning));
|
|
81
|
+
console.log();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Format watch status for terminal output
|
|
86
|
+
*/
|
|
87
|
+
export function formatStatus(status: WatchStatus): string {
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
|
|
90
|
+
lines.push(color("👁️ Mandu Watch Status", colors.bright + colors.blue));
|
|
91
|
+
lines.push(color("─".repeat(40), colors.dim));
|
|
92
|
+
lines.push();
|
|
93
|
+
|
|
94
|
+
const statusIcon = status.active ? "🟢" : "🔴";
|
|
95
|
+
const statusText = status.active ? "Active" : "Inactive";
|
|
96
|
+
|
|
97
|
+
lines.push(`${statusIcon} Status: ${color(statusText, status.active ? colors.green : colors.red)}`);
|
|
98
|
+
|
|
99
|
+
if (status.rootDir) {
|
|
100
|
+
lines.push(`📁 Root: ${color(status.rootDir, colors.cyan)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lines.push(`📊 Files: ${status.fileCount}`);
|
|
104
|
+
|
|
105
|
+
if (status.startedAt) {
|
|
106
|
+
const duration = Math.floor(
|
|
107
|
+
(Date.now() - status.startedAt.getTime()) / 1000
|
|
108
|
+
);
|
|
109
|
+
const minutes = Math.floor(duration / 60);
|
|
110
|
+
const seconds = duration % 60;
|
|
111
|
+
lines.push(
|
|
112
|
+
`⏱️ Uptime: ${minutes > 0 ? `${minutes}m ` : ""}${seconds}s`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (status.recentWarnings.length > 0) {
|
|
117
|
+
lines.push();
|
|
118
|
+
lines.push(
|
|
119
|
+
color(
|
|
120
|
+
`⚠️ Recent Warnings (${status.recentWarnings.length})`,
|
|
121
|
+
colors.yellow
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Show last 5 warnings
|
|
126
|
+
const recent = status.recentWarnings.slice(-5);
|
|
127
|
+
for (const warning of recent) {
|
|
128
|
+
const time = warning.timestamp.toLocaleTimeString();
|
|
129
|
+
lines.push(
|
|
130
|
+
` ${color(time, colors.dim)} [${warning.ruleId}] ${warning.file}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
lines.push();
|
|
135
|
+
lines.push(color("✅ No recent warnings", colors.green));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Print watch status to console
|
|
143
|
+
*/
|
|
144
|
+
export function printStatus(status: WatchStatus): void {
|
|
145
|
+
console.log(formatStatus(status));
|
|
146
|
+
console.log();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Print watch startup message
|
|
151
|
+
*/
|
|
152
|
+
export function printWatchStart(rootDir: string): void {
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(color("👁️ Mandu Watch", colors.bright + colors.blue));
|
|
155
|
+
console.log(color("─".repeat(40), colors.dim));
|
|
156
|
+
console.log(`📁 Watching: ${color(rootDir, colors.cyan)}`);
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(color("Rules active:", colors.dim));
|
|
159
|
+
console.log(" • GENERATED_DIRECT_EDIT - Generated 파일 직접 수정 감지");
|
|
160
|
+
console.log(" • WRONG_SLOT_LOCATION - 잘못된 위치의 Slot 파일 감지");
|
|
161
|
+
console.log(" • SLOT_NAMING - Slot 파일 네이밍 규칙");
|
|
162
|
+
console.log(" • CONTRACT_NAMING - Contract 파일 네이밍 규칙");
|
|
163
|
+
console.log(" • FORBIDDEN_IMPORT - Generated 파일의 금지된 import 감지");
|
|
164
|
+
console.log();
|
|
165
|
+
console.log(
|
|
166
|
+
color("ℹ️ Watch는 경고만 출력합니다. 작업을 차단하지 않습니다.", colors.dim)
|
|
167
|
+
);
|
|
168
|
+
console.log(color(" Press Ctrl+C to stop", colors.dim));
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Print watch stop message
|
|
174
|
+
*/
|
|
175
|
+
export function printWatchStop(): void {
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(color("👁️ Watch stopped", colors.dim));
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Generate JSON status for MCP/API consumption
|
|
183
|
+
*/
|
|
184
|
+
export function generateJsonStatus(status: WatchStatus): string {
|
|
185
|
+
return JSON.stringify(
|
|
186
|
+
{
|
|
187
|
+
active: status.active,
|
|
188
|
+
rootDir: status.rootDir,
|
|
189
|
+
fileCount: status.fileCount,
|
|
190
|
+
startedAt: status.startedAt?.toISOString() || null,
|
|
191
|
+
recentWarnings: status.recentWarnings.map((w) => ({
|
|
192
|
+
ruleId: w.ruleId,
|
|
193
|
+
file: w.file,
|
|
194
|
+
message: w.message,
|
|
195
|
+
event: w.event,
|
|
196
|
+
timestamp: w.timestamp.toISOString(),
|
|
197
|
+
})),
|
|
198
|
+
},
|
|
199
|
+
null,
|
|
200
|
+
2
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a warning handler that prints to console
|
|
206
|
+
*/
|
|
207
|
+
export function createConsoleHandler(): (warning: WatchWarning) => void {
|
|
208
|
+
return (warning) => {
|
|
209
|
+
printWarning(warning);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a warning handler that collects warnings
|
|
215
|
+
*/
|
|
216
|
+
export function createCollectorHandler(): {
|
|
217
|
+
handler: (warning: WatchWarning) => void;
|
|
218
|
+
getWarnings: () => WatchWarning[];
|
|
219
|
+
clear: () => void;
|
|
220
|
+
} {
|
|
221
|
+
const warnings: WatchWarning[] = [];
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
handler: (warning) => {
|
|
225
|
+
warnings.push(warning);
|
|
226
|
+
},
|
|
227
|
+
getWarnings: () => [...warnings],
|
|
228
|
+
clear: () => {
|
|
229
|
+
warnings.length = 0;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|