@mandujs/cli 0.9.12 → 0.9.18
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 +224 -98
- package/package.json +2 -2
- package/src/commands/check.ts +204 -0
- package/src/commands/dev.ts +204 -67
- package/src/commands/doctor.ts +2 -2
- package/src/commands/guard-arch.ts +256 -0
- package/src/commands/routes.ts +218 -0
- package/src/main.ts +138 -42
- package/src/util/fs.ts +23 -4
- package/src/util/output.ts +41 -0
package/src/commands/dev.ts
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
import {
|
|
2
|
-
loadManifest,
|
|
3
2
|
startServer,
|
|
4
3
|
registerApiHandler,
|
|
5
4
|
registerPageLoader,
|
|
6
5
|
registerPageHandler,
|
|
6
|
+
registerLayoutLoader,
|
|
7
7
|
startDevBundler,
|
|
8
8
|
createHMRServer,
|
|
9
9
|
needsHydration,
|
|
10
10
|
loadEnv,
|
|
11
|
+
generateManifest,
|
|
12
|
+
watchFSRoutes,
|
|
13
|
+
clearDefaultRegistry,
|
|
14
|
+
createGuardWatcher,
|
|
15
|
+
getPreset,
|
|
16
|
+
type RoutesManifest,
|
|
17
|
+
type GuardConfig,
|
|
18
|
+
type GuardPreset,
|
|
11
19
|
} from "@mandujs/core";
|
|
12
|
-
import { resolveFromCwd } from "../util/fs";
|
|
20
|
+
import { isDirectory, resolveFromCwd } from "../util/fs";
|
|
21
|
+
import { resolveOutputFormat, type OutputFormat } from "../util/output";
|
|
13
22
|
import path from "path";
|
|
14
23
|
|
|
15
|
-
export interface DevOptions {
|
|
16
|
-
port?: number;
|
|
17
|
-
/** HMR 비활성화 */
|
|
18
|
-
noHmr?: boolean;
|
|
19
|
-
|
|
24
|
+
export interface DevOptions {
|
|
25
|
+
port?: number;
|
|
26
|
+
/** HMR 비활성화 */
|
|
27
|
+
noHmr?: boolean;
|
|
28
|
+
/** FS Routes 비활성화 (레거시 모드) */
|
|
29
|
+
legacy?: boolean;
|
|
30
|
+
/** Architecture Guard 활성화 */
|
|
31
|
+
guard?: boolean;
|
|
32
|
+
/** Guard 프리셋 */
|
|
33
|
+
guardPreset?: GuardPreset;
|
|
34
|
+
/** Guard 출력 형식 */
|
|
35
|
+
guardFormat?: OutputFormat;
|
|
36
|
+
}
|
|
20
37
|
|
|
21
38
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
22
|
-
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
23
39
|
const rootDir = resolveFromCwd(".");
|
|
24
40
|
|
|
25
|
-
console.log(`🥟 Mandu Dev Server`);
|
|
41
|
+
console.log(`🥟 Mandu Dev Server (FS Routes)`);
|
|
26
42
|
|
|
27
43
|
// .env 파일 로드
|
|
28
44
|
const envResult = await loadEnv({
|
|
@@ -34,50 +50,93 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
34
50
|
console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
// FS Routes 스캔
|
|
54
|
+
console.log(`📂 app/ 폴더 스캔 중...`);
|
|
38
55
|
|
|
39
|
-
const result = await
|
|
56
|
+
const result = await generateManifest(rootDir, {
|
|
57
|
+
skipLegacy: true,
|
|
58
|
+
});
|
|
40
59
|
|
|
41
|
-
if (
|
|
42
|
-
console.
|
|
43
|
-
|
|
60
|
+
if (result.manifest.routes.length === 0) {
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log("📭 라우트가 없습니다.");
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("💡 app/ 폴더에 page.tsx 파일을 생성하세요:");
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log(" app/page.tsx → /");
|
|
67
|
+
console.log(" app/blog/page.tsx → /blog");
|
|
68
|
+
console.log(" app/api/users/route.ts → /api/users");
|
|
69
|
+
console.log("");
|
|
44
70
|
process.exit(1);
|
|
45
71
|
}
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
console.log(`✅
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
let manifest = result.manifest;
|
|
74
|
+
console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
|
|
75
|
+
|
|
76
|
+
// Layout 경로 추적 (중복 등록 방지)
|
|
77
|
+
const registeredLayouts = new Set<string>();
|
|
78
|
+
|
|
79
|
+
// 핸들러 등록 함수
|
|
80
|
+
const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
|
|
81
|
+
// 리로드 시 레이아웃 캐시 클리어
|
|
82
|
+
if (isReload) {
|
|
83
|
+
registeredLayouts.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const route of manifest.routes) {
|
|
87
|
+
if (route.kind === "api") {
|
|
88
|
+
const modulePath = path.resolve(rootDir, route.module);
|
|
89
|
+
try {
|
|
90
|
+
// 캐시 무효화 (HMR용)
|
|
91
|
+
delete require.cache[modulePath];
|
|
92
|
+
const module = await import(modulePath);
|
|
93
|
+
registerApiHandler(route.id, module.default || module.handler || module);
|
|
94
|
+
console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
|
|
97
|
+
}
|
|
98
|
+
} else if (route.kind === "page" && route.componentModule) {
|
|
99
|
+
const componentPath = path.resolve(rootDir, route.componentModule);
|
|
100
|
+
const isIsland = needsHydration(route);
|
|
101
|
+
const hasLayout = route.layoutChain && route.layoutChain.length > 0;
|
|
102
|
+
|
|
103
|
+
// Layout 로더 등록
|
|
104
|
+
if (route.layoutChain) {
|
|
105
|
+
for (const layoutPath of route.layoutChain) {
|
|
106
|
+
if (!registeredLayouts.has(layoutPath)) {
|
|
107
|
+
const absLayoutPath = path.resolve(rootDir, layoutPath);
|
|
108
|
+
registerLayoutLoader(layoutPath, async () => {
|
|
109
|
+
// 캐시 무효화 (HMR용)
|
|
110
|
+
delete require.cache[absLayoutPath];
|
|
111
|
+
return import(absLayoutPath);
|
|
112
|
+
});
|
|
113
|
+
registeredLayouts.add(layoutPath);
|
|
114
|
+
console.log(` 🎨 Layout: ${layoutPath}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
|
|
120
|
+
if (route.slotModule) {
|
|
121
|
+
registerPageHandler(route.id, async () => {
|
|
122
|
+
delete require.cache[componentPath];
|
|
123
|
+
const module = await import(componentPath);
|
|
124
|
+
return module.default;
|
|
125
|
+
});
|
|
126
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
|
|
127
|
+
} else {
|
|
128
|
+
registerPageLoader(route.id, () => {
|
|
129
|
+
delete require.cache[componentPath];
|
|
130
|
+
return import(componentPath);
|
|
131
|
+
});
|
|
132
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
|
|
133
|
+
}
|
|
77
134
|
}
|
|
78
135
|
}
|
|
79
|
-
}
|
|
136
|
+
};
|
|
80
137
|
|
|
138
|
+
// 초기 핸들러 등록
|
|
139
|
+
await registerHandlers(manifest);
|
|
81
140
|
console.log("");
|
|
82
141
|
|
|
83
142
|
const port = options.port || Number(process.env.PORT) || 3000;
|
|
@@ -98,28 +157,28 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
98
157
|
devBundler = await startDevBundler({
|
|
99
158
|
rootDir,
|
|
100
159
|
manifest,
|
|
101
|
-
onRebuild: (result) => {
|
|
102
|
-
if (result.success) {
|
|
103
|
-
if (result.routeId === "*") {
|
|
104
|
-
hmrServer?.broadcast({
|
|
105
|
-
type: "reload",
|
|
106
|
-
data: {
|
|
107
|
-
timestamp: Date.now(),
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
} else {
|
|
111
|
-
hmrServer?.broadcast({
|
|
112
|
-
type: "island-update",
|
|
113
|
-
data: {
|
|
114
|
-
routeId: result.routeId,
|
|
115
|
-
timestamp: Date.now(),
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
hmrServer?.broadcast({
|
|
121
|
-
type: "error",
|
|
122
|
-
data: {
|
|
160
|
+
onRebuild: (result) => {
|
|
161
|
+
if (result.success) {
|
|
162
|
+
if (result.routeId === "*") {
|
|
163
|
+
hmrServer?.broadcast({
|
|
164
|
+
type: "reload",
|
|
165
|
+
data: {
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
hmrServer?.broadcast({
|
|
171
|
+
type: "island-update",
|
|
172
|
+
data: {
|
|
173
|
+
routeId: result.routeId,
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
hmrServer?.broadcast({
|
|
180
|
+
type: "error",
|
|
181
|
+
data: {
|
|
123
182
|
routeId: result.routeId,
|
|
124
183
|
message: result.error,
|
|
125
184
|
},
|
|
@@ -147,12 +206,90 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
147
206
|
bundleManifest: devBundler?.initialBuild.manifest,
|
|
148
207
|
});
|
|
149
208
|
|
|
209
|
+
// Architecture Guard 실시간 감시 (선택적)
|
|
210
|
+
let archGuardWatcher: ReturnType<typeof createGuardWatcher> | null = null;
|
|
211
|
+
|
|
212
|
+
if (options.guard !== false) {
|
|
213
|
+
const guardPreset = options.guardPreset || "mandu";
|
|
214
|
+
const guardFormat = resolveOutputFormat(options.guardFormat);
|
|
215
|
+
const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
|
|
216
|
+
const guardConfig: GuardConfig = {
|
|
217
|
+
preset: guardPreset,
|
|
218
|
+
srcDir: "src",
|
|
219
|
+
realtime: true,
|
|
220
|
+
realtimeOutput: guardFormat,
|
|
221
|
+
fsRoutes: enableFsRoutes
|
|
222
|
+
? {
|
|
223
|
+
noPageToPage: true,
|
|
224
|
+
pageCanImport: ["widgets", "features", "entities", "shared"],
|
|
225
|
+
layoutCanImport: ["widgets", "shared"],
|
|
226
|
+
}
|
|
227
|
+
: undefined,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
console.log(`🛡️ Architecture Guard 활성화 (${guardPreset})`);
|
|
231
|
+
|
|
232
|
+
archGuardWatcher = createGuardWatcher({
|
|
233
|
+
config: guardConfig,
|
|
234
|
+
rootDir,
|
|
235
|
+
onViolation: (violation) => {
|
|
236
|
+
// 실시간 경고는 watcher 내부에서 처리
|
|
237
|
+
},
|
|
238
|
+
onFileAnalyzed: (analysis, violations) => {
|
|
239
|
+
if (violations.length > 0) {
|
|
240
|
+
// HMR 에러로 브로드캐스트
|
|
241
|
+
hmrServer?.broadcast({
|
|
242
|
+
type: "guard-violation",
|
|
243
|
+
data: {
|
|
244
|
+
file: analysis.filePath,
|
|
245
|
+
violations: violations.map((v) => ({
|
|
246
|
+
line: v.line,
|
|
247
|
+
message: `${v.fromLayer} → ${v.toLayer}: ${v.ruleDescription}`,
|
|
248
|
+
})),
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
archGuardWatcher.start();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// FS Routes 실시간 감시
|
|
259
|
+
const routesWatcher = await watchFSRoutes(rootDir, {
|
|
260
|
+
skipLegacy: true,
|
|
261
|
+
onChange: async (result) => {
|
|
262
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
263
|
+
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
264
|
+
|
|
265
|
+
// 레지스트리 클리어 (layout 캐시 포함)
|
|
266
|
+
clearDefaultRegistry();
|
|
267
|
+
|
|
268
|
+
// 새 매니페스트로 서버 업데이트
|
|
269
|
+
manifest = result.manifest;
|
|
270
|
+
console.log(` 📋 라우트: ${manifest.routes.length}개`);
|
|
271
|
+
|
|
272
|
+
// 라우트 재등록 (isReload = true)
|
|
273
|
+
await registerHandlers(manifest, true);
|
|
274
|
+
|
|
275
|
+
// HMR 브로드캐스트 (전체 리로드)
|
|
276
|
+
if (hmrServer) {
|
|
277
|
+
hmrServer.broadcast({
|
|
278
|
+
type: "reload",
|
|
279
|
+
data: { timestamp: Date.now() },
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
150
285
|
// 정리 함수
|
|
151
286
|
const cleanup = () => {
|
|
152
287
|
console.log("\n🛑 서버 종료 중...");
|
|
153
288
|
server.stop();
|
|
154
289
|
devBundler?.close();
|
|
155
290
|
hmrServer?.close();
|
|
291
|
+
routesWatcher.close();
|
|
292
|
+
archGuardWatcher?.close();
|
|
156
293
|
process.exit(0);
|
|
157
294
|
};
|
|
158
295
|
|
package/src/commands/doctor.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
runGuardCheck,
|
|
11
11
|
analyzeViolations,
|
|
12
12
|
printDoctorReport,
|
|
13
|
-
|
|
13
|
+
generateDoctorMarkdownReport,
|
|
14
14
|
initializeBrain,
|
|
15
15
|
getBrain,
|
|
16
16
|
} from "../../../core/src/index";
|
|
@@ -108,7 +108,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
case "markdown": {
|
|
111
|
-
const md =
|
|
111
|
+
const md = generateDoctorMarkdownReport(analysis);
|
|
112
112
|
|
|
113
113
|
if (output) {
|
|
114
114
|
await fs.writeFile(output, md, "utf-8");
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mandu guard arch - Architecture Guard Command
|
|
3
|
+
*
|
|
4
|
+
* 실시간 아키텍처 감시 및 일회성 검사
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createGuardWatcher,
|
|
9
|
+
checkDirectory,
|
|
10
|
+
printReport,
|
|
11
|
+
formatReportForAgent,
|
|
12
|
+
formatReportAsAgentJSON,
|
|
13
|
+
getPreset,
|
|
14
|
+
listPresets,
|
|
15
|
+
createScanRecord,
|
|
16
|
+
addScanRecord,
|
|
17
|
+
loadStatistics,
|
|
18
|
+
analyzeTrend,
|
|
19
|
+
calculateLayerStatistics,
|
|
20
|
+
generateGuardMarkdownReport,
|
|
21
|
+
generateHTMLReport,
|
|
22
|
+
type GuardConfig,
|
|
23
|
+
type GuardPreset,
|
|
24
|
+
} from "@mandujs/core";
|
|
25
|
+
import { writeFile } from "fs/promises";
|
|
26
|
+
import { isDirectory, resolveFromCwd } from "../util/fs";
|
|
27
|
+
import { resolveOutputFormat, type OutputFormat } from "../util/output";
|
|
28
|
+
import path from "path";
|
|
29
|
+
|
|
30
|
+
export interface GuardArchOptions {
|
|
31
|
+
/** 프리셋 이름 */
|
|
32
|
+
preset?: GuardPreset;
|
|
33
|
+
/** 실시간 감시 모드 */
|
|
34
|
+
watch?: boolean;
|
|
35
|
+
/** CI 모드 (에러 시 exit 1) */
|
|
36
|
+
ci?: boolean;
|
|
37
|
+
/** 출력 형식: console, agent, json */
|
|
38
|
+
format?: OutputFormat;
|
|
39
|
+
/** 조용히 (요약만 출력) */
|
|
40
|
+
quiet?: boolean;
|
|
41
|
+
/** 소스 디렉토리 */
|
|
42
|
+
srcDir?: string;
|
|
43
|
+
/** 프리셋 목록 출력 */
|
|
44
|
+
listPresets?: boolean;
|
|
45
|
+
/** 리포트 파일 출력 */
|
|
46
|
+
output?: string;
|
|
47
|
+
/** 리포트 형식: json, markdown, html */
|
|
48
|
+
reportFormat?: "json" | "markdown" | "html";
|
|
49
|
+
/** 통계 저장 (트렌드 분석용) */
|
|
50
|
+
saveStats?: boolean;
|
|
51
|
+
/** 트렌드 분석 표시 */
|
|
52
|
+
showTrend?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
|
|
56
|
+
const {
|
|
57
|
+
preset = "mandu",
|
|
58
|
+
watch = false,
|
|
59
|
+
ci = false,
|
|
60
|
+
format,
|
|
61
|
+
quiet = false,
|
|
62
|
+
srcDir = "src",
|
|
63
|
+
listPresets: showPresets = false,
|
|
64
|
+
output,
|
|
65
|
+
reportFormat = "markdown",
|
|
66
|
+
saveStats = false,
|
|
67
|
+
showTrend = false,
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
const rootDir = resolveFromCwd(".");
|
|
71
|
+
const resolvedFormat = resolveOutputFormat(format);
|
|
72
|
+
const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
|
|
73
|
+
|
|
74
|
+
// 프리셋 목록 출력
|
|
75
|
+
if (showPresets) {
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("🛡️ Mandu Guard - Available Presets");
|
|
78
|
+
console.log("");
|
|
79
|
+
|
|
80
|
+
const presets = listPresets();
|
|
81
|
+
for (const p of presets) {
|
|
82
|
+
const presetDef = getPreset(p.name);
|
|
83
|
+
console.log(` ${p.name === "fsd" ? "✨ " : " "}${p.name}`);
|
|
84
|
+
console.log(` ${p.description}`);
|
|
85
|
+
console.log(` Layers: ${presetDef.hierarchy.join(" → ")}`);
|
|
86
|
+
console.log("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log("Usage: bunx mandu guard arch --preset <name>");
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (resolvedFormat === "console") {
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("🛡️ Mandu Guard - Architecture Checker");
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(`📋 Preset: ${preset}`);
|
|
98
|
+
console.log(`📂 Source: ${srcDir}/`);
|
|
99
|
+
console.log(`🔧 Mode: ${watch ? "Watch" : "Check"}`);
|
|
100
|
+
console.log("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Guard 설정
|
|
104
|
+
const config: GuardConfig = {
|
|
105
|
+
preset,
|
|
106
|
+
srcDir,
|
|
107
|
+
realtime: watch,
|
|
108
|
+
realtimeOutput: resolvedFormat,
|
|
109
|
+
fsRoutes: enableFsRoutes
|
|
110
|
+
? {
|
|
111
|
+
noPageToPage: true,
|
|
112
|
+
pageCanImport: ["widgets", "features", "entities", "shared"],
|
|
113
|
+
layoutCanImport: ["widgets", "shared"],
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// 실시간 감시 모드
|
|
119
|
+
if (watch) {
|
|
120
|
+
if (resolvedFormat === "console") {
|
|
121
|
+
console.log("👁️ Watching for architecture violations...");
|
|
122
|
+
console.log(" Press Ctrl+C to stop\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const watcher = createGuardWatcher({
|
|
126
|
+
config,
|
|
127
|
+
rootDir,
|
|
128
|
+
onViolation: (violation) => {
|
|
129
|
+
// 실시간 위반 출력은 watcher 내부에서 처리됨
|
|
130
|
+
},
|
|
131
|
+
onFileAnalyzed: (analysis, violations) => {
|
|
132
|
+
if (resolvedFormat === "console" && violations.length > 0 && !quiet) {
|
|
133
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
134
|
+
console.log(`[${timestamp}] ${analysis.filePath}: ${violations.length} violation(s)`);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
watcher.start();
|
|
140
|
+
|
|
141
|
+
// Ctrl+C 핸들링
|
|
142
|
+
process.on("SIGINT", () => {
|
|
143
|
+
console.log("\n🛑 Guard stopped");
|
|
144
|
+
watcher.close();
|
|
145
|
+
process.exit(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 계속 실행
|
|
149
|
+
return new Promise(() => {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 일회성 검사 모드
|
|
153
|
+
if (resolvedFormat === "console" && !quiet) {
|
|
154
|
+
console.log("🔍 Scanning for architecture violations...\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const report = await checkDirectory(config, rootDir);
|
|
158
|
+
const presetDef = getPreset(preset);
|
|
159
|
+
|
|
160
|
+
// 출력 형식에 따른 리포트 출력
|
|
161
|
+
switch (resolvedFormat) {
|
|
162
|
+
case "json":
|
|
163
|
+
console.log(formatReportAsAgentJSON(report, preset));
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "agent":
|
|
167
|
+
console.log(formatReportForAgent(report, preset));
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case "console":
|
|
171
|
+
default:
|
|
172
|
+
if (quiet) {
|
|
173
|
+
// 요약만 출력
|
|
174
|
+
console.log(`Files analyzed: ${report.filesAnalyzed}`);
|
|
175
|
+
console.log(`Violations: ${report.totalViolations}`);
|
|
176
|
+
console.log(` Errors: ${report.bySeverity.error}`);
|
|
177
|
+
console.log(` Warnings: ${report.bySeverity.warn}`);
|
|
178
|
+
console.log(` Info: ${report.bySeverity.info}`);
|
|
179
|
+
} else {
|
|
180
|
+
printReport(report, presetDef.hierarchy);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 통계 저장
|
|
186
|
+
if (saveStats) {
|
|
187
|
+
const scanRecord = createScanRecord(report, preset);
|
|
188
|
+
await addScanRecord(rootDir, scanRecord);
|
|
189
|
+
console.log("📊 Statistics saved to .mandu/guard-stats.json");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 트렌드 분석
|
|
193
|
+
let trend = null;
|
|
194
|
+
let layerStats = null;
|
|
195
|
+
|
|
196
|
+
if (showTrend) {
|
|
197
|
+
const store = await loadStatistics(rootDir);
|
|
198
|
+
trend = analyzeTrend(store.records, 7);
|
|
199
|
+
layerStats = calculateLayerStatistics(report.violations, presetDef.hierarchy);
|
|
200
|
+
|
|
201
|
+
if (trend) {
|
|
202
|
+
console.log("");
|
|
203
|
+
console.log("📈 Trend Analysis (7 days):");
|
|
204
|
+
const trendEmoji = trend.trend === "improving" ? "📉" : trend.trend === "degrading" ? "📈" : "➡️";
|
|
205
|
+
console.log(` Status: ${trendEmoji} ${trend.trend.toUpperCase()}`);
|
|
206
|
+
console.log(` Change: ${trend.violationDelta >= 0 ? "+" : ""}${trend.violationDelta} (${trend.violationChangePercent >= 0 ? "+" : ""}${trend.violationChangePercent}%)`);
|
|
207
|
+
|
|
208
|
+
if (trend.recommendations.length > 0) {
|
|
209
|
+
console.log(" 💡 Recommendations:");
|
|
210
|
+
for (const rec of trend.recommendations) {
|
|
211
|
+
console.log(` - ${rec}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 리포트 파일 출력
|
|
218
|
+
if (output) {
|
|
219
|
+
let reportContent: string;
|
|
220
|
+
|
|
221
|
+
switch (reportFormat) {
|
|
222
|
+
case "json":
|
|
223
|
+
reportContent = formatReportAsAgentJSON(report, preset);
|
|
224
|
+
break;
|
|
225
|
+
case "html":
|
|
226
|
+
reportContent = generateHTMLReport(report, trend, layerStats ?? undefined);
|
|
227
|
+
break;
|
|
228
|
+
case "markdown":
|
|
229
|
+
default:
|
|
230
|
+
reportContent = generateGuardMarkdownReport(report, trend, layerStats ?? undefined);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await writeFile(output, reportContent);
|
|
235
|
+
console.log(`\n📄 Report saved to ${output}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// CI 모드에서 에러가 있으면 실패
|
|
239
|
+
if (ci && report.bySeverity.error > 0) {
|
|
240
|
+
console.log("\n❌ Architecture check failed");
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (report.totalViolations === 0) {
|
|
245
|
+
console.log("\n✅ Architecture check passed");
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (report.bySeverity.error > 0) {
|
|
250
|
+
console.log(`\n⚠️ ${report.bySeverity.error} error(s) found - please fix before continuing`);
|
|
251
|
+
return !ci;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`\n⚠️ ${report.totalViolations} issue(s) found`);
|
|
255
|
+
return true;
|
|
256
|
+
}
|