@mandujs/cli 0.12.2 → 0.13.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.ko.md +234 -234
- package/README.md +354 -354
- package/package.json +2 -2
- package/src/commands/contract.ts +173 -173
- package/src/commands/dev.ts +8 -68
- package/src/commands/doctor.ts +27 -27
- package/src/commands/guard-arch.ts +303 -303
- package/src/commands/guard-check.ts +3 -3
- package/src/commands/monitor.ts +300 -300
- package/src/commands/openapi.ts +107 -107
- package/src/commands/registry.ts +367 -357
- package/src/commands/routes.ts +228 -228
- package/src/commands/start.ts +184 -0
- package/src/errors/codes.ts +35 -35
- package/src/errors/index.ts +2 -2
- package/src/errors/messages.ts +143 -143
- package/src/hooks/index.ts +17 -17
- package/src/hooks/preaction.ts +256 -256
- package/src/main.ts +37 -34
- package/src/terminal/banner.ts +166 -166
- package/src/terminal/help.ts +306 -306
- package/src/terminal/index.ts +71 -71
- package/src/terminal/output.ts +295 -295
- package/src/terminal/palette.ts +30 -30
- package/src/terminal/progress.ts +327 -327
- package/src/terminal/stream-writer.ts +214 -214
- package/src/terminal/table.ts +354 -354
- package/src/terminal/theme.ts +142 -142
- package/src/util/bun.ts +6 -6
- package/src/util/fs.ts +23 -23
- package/src/util/handlers.ts +96 -0
- package/src/util/manifest.ts +52 -52
- package/src/util/output.ts +22 -22
- package/src/util/port.ts +71 -71
- package/templates/default/AGENTS.md +96 -96
- package/templates/default/app/api/health/route.ts +13 -13
- package/templates/default/app/globals.css +49 -49
- package/templates/default/app/layout.tsx +27 -27
- package/templates/default/app/page.tsx +38 -38
- package/templates/default/package.json +1 -0
- package/templates/default/src/client/shared/lib/utils.ts +16 -16
- package/templates/default/src/client/shared/ui/button.tsx +57 -57
- package/templates/default/src/client/shared/ui/card.tsx +78 -78
- package/templates/default/src/client/shared/ui/index.ts +21 -21
- package/templates/default/src/client/shared/ui/input.tsx +24 -24
- package/templates/default/tests/example.test.ts +58 -58
- package/templates/default/tests/helpers.ts +52 -52
- package/templates/default/tests/setup.ts +9 -9
- package/templates/default/tsconfig.json +12 -14
- package/templates/default/apps/server/main.ts +0 -67
- package/templates/default/apps/web/entry.tsx +0 -35
|
@@ -1,303 +1,303 @@
|
|
|
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
|
-
validateAndReport,
|
|
23
|
-
type GuardConfig,
|
|
24
|
-
type GuardPreset,
|
|
25
|
-
} from "@mandujs/core";
|
|
26
|
-
import { writeFile } from "fs/promises";
|
|
27
|
-
import { isDirectory, resolveFromCwd } from "../util/fs";
|
|
28
|
-
import { resolveOutputFormat, type OutputFormat } from "../util/output";
|
|
29
|
-
import path from "path";
|
|
30
|
-
|
|
31
|
-
export interface GuardArchOptions {
|
|
32
|
-
/** 프리셋 이름 */
|
|
33
|
-
preset?: GuardPreset;
|
|
34
|
-
/** 실시간 감시 모드 */
|
|
35
|
-
watch?: boolean;
|
|
36
|
-
/** CI 모드 (에러 시 exit 1) */
|
|
37
|
-
ci?: boolean;
|
|
38
|
-
/** 출력 형식: console, agent, json */
|
|
39
|
-
format?: OutputFormat;
|
|
40
|
-
/** 조용히 (요약만 출력) */
|
|
41
|
-
quiet?: boolean;
|
|
42
|
-
/** 소스 디렉토리 */
|
|
43
|
-
srcDir?: string;
|
|
44
|
-
/** 프리셋 목록 출력 */
|
|
45
|
-
listPresets?: boolean;
|
|
46
|
-
/** 리포트 파일 출력 */
|
|
47
|
-
output?: string;
|
|
48
|
-
/** 리포트 형식: json, markdown, html */
|
|
49
|
-
reportFormat?: "json" | "markdown" | "html";
|
|
50
|
-
/** 통계 저장 (트렌드 분석용) */
|
|
51
|
-
saveStats?: boolean;
|
|
52
|
-
/** 트렌드 분석 표시 */
|
|
53
|
-
showTrend?: boolean;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function inferReportFormat(output?: string): "json" | "markdown" | "html" | undefined {
|
|
57
|
-
if (!output) return undefined;
|
|
58
|
-
const ext = path.extname(output).toLowerCase();
|
|
59
|
-
if (ext === ".json") return "json";
|
|
60
|
-
if (ext === ".html" || ext === ".htm") return "html";
|
|
61
|
-
if (ext === ".md" || ext === ".markdown") return "markdown";
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
|
|
66
|
-
const rootDir = resolveFromCwd(".");
|
|
67
|
-
const {
|
|
68
|
-
watch = false,
|
|
69
|
-
ci = process.env.CI === "true",
|
|
70
|
-
format,
|
|
71
|
-
quiet = false,
|
|
72
|
-
listPresets: showPresets = false,
|
|
73
|
-
output,
|
|
74
|
-
reportFormat = inferReportFormat(options.output) ?? "markdown",
|
|
75
|
-
saveStats = false,
|
|
76
|
-
showTrend = false,
|
|
77
|
-
} = options;
|
|
78
|
-
const resolvedFormat = resolveOutputFormat(format);
|
|
79
|
-
const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
|
|
80
|
-
|
|
81
|
-
// 프리셋 목록 출력
|
|
82
|
-
if (showPresets) {
|
|
83
|
-
console.log("");
|
|
84
|
-
console.log("🛡️ Mandu Guard - Available Presets");
|
|
85
|
-
console.log("");
|
|
86
|
-
|
|
87
|
-
const presets = listPresets();
|
|
88
|
-
for (const p of presets) {
|
|
89
|
-
const presetDef = getPreset(p.name);
|
|
90
|
-
console.log(` ${p.name === "fsd" ? "✨ " : " "}${p.name}`);
|
|
91
|
-
console.log(` ${p.description}`);
|
|
92
|
-
console.log(` Layers: ${presetDef.hierarchy.join(" → ")}`);
|
|
93
|
-
console.log("");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
console.log("Usage: set guard.preset in mandu.config to choose a preset");
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const fileConfig = await validateAndReport(rootDir);
|
|
101
|
-
if (!fileConfig) return false;
|
|
102
|
-
const guardConfigFromFile = fileConfig.guard ?? {};
|
|
103
|
-
|
|
104
|
-
const preset = options.preset ?? guardConfigFromFile.preset ?? "mandu";
|
|
105
|
-
const srcDir = options.srcDir ?? guardConfigFromFile.srcDir ?? "src";
|
|
106
|
-
|
|
107
|
-
if (resolvedFormat === "console") {
|
|
108
|
-
console.log("");
|
|
109
|
-
console.log("🛡️ Mandu Guard - Architecture Checker");
|
|
110
|
-
console.log("");
|
|
111
|
-
console.log(`📋 Preset: ${preset}`);
|
|
112
|
-
console.log(`📂 Source: ${srcDir}/`);
|
|
113
|
-
console.log(`🔧 Mode: ${watch ? "Watch" : "Check"}`);
|
|
114
|
-
console.log("");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Guard 설정
|
|
118
|
-
const guardConfig: GuardConfig = {
|
|
119
|
-
preset,
|
|
120
|
-
srcDir,
|
|
121
|
-
realtime: watch,
|
|
122
|
-
realtimeOutput: resolvedFormat,
|
|
123
|
-
exclude: guardConfigFromFile.exclude,
|
|
124
|
-
fsRoutes: enableFsRoutes
|
|
125
|
-
? {
|
|
126
|
-
noPageToPage: true,
|
|
127
|
-
pageCanImport: [
|
|
128
|
-
"client/pages",
|
|
129
|
-
"client/widgets",
|
|
130
|
-
"client/features",
|
|
131
|
-
"client/entities",
|
|
132
|
-
"client/shared",
|
|
133
|
-
"shared/contracts",
|
|
134
|
-
"shared/types",
|
|
135
|
-
"shared/utils/client",
|
|
136
|
-
],
|
|
137
|
-
layoutCanImport: [
|
|
138
|
-
"client/app",
|
|
139
|
-
"client/widgets",
|
|
140
|
-
"client/shared",
|
|
141
|
-
"shared/contracts",
|
|
142
|
-
"shared/types",
|
|
143
|
-
"shared/utils/client",
|
|
144
|
-
],
|
|
145
|
-
routeCanImport: [
|
|
146
|
-
"server/api",
|
|
147
|
-
"server/application",
|
|
148
|
-
"server/domain",
|
|
149
|
-
"server/infra",
|
|
150
|
-
"server/core",
|
|
151
|
-
"shared/contracts",
|
|
152
|
-
"shared/schema",
|
|
153
|
-
"shared/types",
|
|
154
|
-
"shared/utils/client",
|
|
155
|
-
"shared/utils/server",
|
|
156
|
-
"shared/env",
|
|
157
|
-
],
|
|
158
|
-
}
|
|
159
|
-
: undefined,
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// 실시간 감시 모드
|
|
163
|
-
if (watch) {
|
|
164
|
-
if (resolvedFormat === "console") {
|
|
165
|
-
console.log("👁️ Watching for architecture violations...");
|
|
166
|
-
console.log(" Press Ctrl+C to stop\n");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const watcher = createGuardWatcher({
|
|
170
|
-
config: guardConfig,
|
|
171
|
-
rootDir,
|
|
172
|
-
onViolation: (violation) => {
|
|
173
|
-
// 실시간 위반 출력은 watcher 내부에서 처리됨
|
|
174
|
-
},
|
|
175
|
-
onFileAnalyzed: (analysis, violations) => {
|
|
176
|
-
if (resolvedFormat === "console" && violations.length > 0 && !quiet) {
|
|
177
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
178
|
-
console.log(`[${timestamp}] ${analysis.filePath}: ${violations.length} violation(s)`);
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
watcher.start();
|
|
184
|
-
|
|
185
|
-
// Ctrl+C 핸들링
|
|
186
|
-
process.on("SIGINT", () => {
|
|
187
|
-
if (resolvedFormat === "console") {
|
|
188
|
-
console.log("\n🛑 Guard stopped");
|
|
189
|
-
}
|
|
190
|
-
watcher.close();
|
|
191
|
-
process.exit(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// 계속 실행
|
|
195
|
-
return new Promise(() => {});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 일회성 검사 모드
|
|
199
|
-
if (resolvedFormat === "console" && !quiet) {
|
|
200
|
-
console.log("🔍 Scanning for architecture violations...\n");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const report = await checkDirectory(guardConfig, rootDir);
|
|
204
|
-
const presetDef = getPreset(preset);
|
|
205
|
-
|
|
206
|
-
// 출력 형식에 따른 리포트 출력
|
|
207
|
-
switch (resolvedFormat) {
|
|
208
|
-
case "json":
|
|
209
|
-
console.log(formatReportAsAgentJSON(report, preset));
|
|
210
|
-
break;
|
|
211
|
-
|
|
212
|
-
case "agent":
|
|
213
|
-
console.log(formatReportForAgent(report, preset));
|
|
214
|
-
break;
|
|
215
|
-
|
|
216
|
-
case "console":
|
|
217
|
-
default:
|
|
218
|
-
if (quiet) {
|
|
219
|
-
// 요약만 출력
|
|
220
|
-
console.log(`Files analyzed: ${report.filesAnalyzed}`);
|
|
221
|
-
console.log(`Violations: ${report.totalViolations}`);
|
|
222
|
-
console.log(` Errors: ${report.bySeverity.error}`);
|
|
223
|
-
console.log(` Warnings: ${report.bySeverity.warn}`);
|
|
224
|
-
console.log(` Info: ${report.bySeverity.info}`);
|
|
225
|
-
} else {
|
|
226
|
-
printReport(report, presetDef.hierarchy);
|
|
227
|
-
}
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// 통계 저장
|
|
232
|
-
if (saveStats) {
|
|
233
|
-
const scanRecord = createScanRecord(report, preset);
|
|
234
|
-
await addScanRecord(rootDir, scanRecord);
|
|
235
|
-
console.log("📊 Statistics saved to .mandu/guard-stats.json");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 트렌드 분석
|
|
239
|
-
let trend = null;
|
|
240
|
-
let layerStats = null;
|
|
241
|
-
|
|
242
|
-
if (showTrend) {
|
|
243
|
-
const store = await loadStatistics(rootDir);
|
|
244
|
-
trend = analyzeTrend(store.records, 7);
|
|
245
|
-
layerStats = calculateLayerStatistics(report.violations, presetDef.hierarchy);
|
|
246
|
-
|
|
247
|
-
if (trend) {
|
|
248
|
-
console.log("");
|
|
249
|
-
console.log("📈 Trend Analysis (7 days):");
|
|
250
|
-
const trendEmoji = trend.trend === "improving" ? "📉" : trend.trend === "degrading" ? "📈" : "➡️";
|
|
251
|
-
console.log(` Status: ${trendEmoji} ${trend.trend.toUpperCase()}`);
|
|
252
|
-
console.log(` Change: ${trend.violationDelta >= 0 ? "+" : ""}${trend.violationDelta} (${trend.violationChangePercent >= 0 ? "+" : ""}${trend.violationChangePercent}%)`);
|
|
253
|
-
|
|
254
|
-
if (trend.recommendations.length > 0) {
|
|
255
|
-
console.log(" 💡 Recommendations:");
|
|
256
|
-
for (const rec of trend.recommendations) {
|
|
257
|
-
console.log(` - ${rec}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// 리포트 파일 출력
|
|
264
|
-
if (output) {
|
|
265
|
-
let reportContent: string;
|
|
266
|
-
|
|
267
|
-
switch (reportFormat) {
|
|
268
|
-
case "json":
|
|
269
|
-
reportContent = formatReportAsAgentJSON(report, preset);
|
|
270
|
-
break;
|
|
271
|
-
case "html":
|
|
272
|
-
reportContent = generateHTMLReport(report, trend, layerStats ?? undefined);
|
|
273
|
-
break;
|
|
274
|
-
case "markdown":
|
|
275
|
-
default:
|
|
276
|
-
reportContent = generateGuardMarkdownReport(report, trend, layerStats ?? undefined);
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
await writeFile(output, reportContent);
|
|
281
|
-
console.log(`\n📄 Report saved to ${output}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// CI 모드에서 에러가 있으면 실패
|
|
285
|
-
const hasErrors = report.bySeverity.error > 0;
|
|
286
|
-
const hasWarnings = report.bySeverity.warn > 0;
|
|
287
|
-
|
|
288
|
-
if (report.totalViolations === 0) {
|
|
289
|
-
console.log("\n✅ Architecture check passed");
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (hasErrors || (ci && hasWarnings)) {
|
|
294
|
-
const reason = hasErrors
|
|
295
|
-
? `${report.bySeverity.error} error(s)`
|
|
296
|
-
: `${report.bySeverity.warn} warning(s)`;
|
|
297
|
-
console.log(`\n❌ Architecture check failed: ${reason}`);
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
console.log(`\n⚠️ ${report.totalViolations} issue(s) found`);
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
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
|
+
validateAndReport,
|
|
23
|
+
type GuardConfig,
|
|
24
|
+
type GuardPreset,
|
|
25
|
+
} from "@mandujs/core";
|
|
26
|
+
import { writeFile } from "fs/promises";
|
|
27
|
+
import { isDirectory, resolveFromCwd } from "../util/fs";
|
|
28
|
+
import { resolveOutputFormat, type OutputFormat } from "../util/output";
|
|
29
|
+
import path from "path";
|
|
30
|
+
|
|
31
|
+
export interface GuardArchOptions {
|
|
32
|
+
/** 프리셋 이름 */
|
|
33
|
+
preset?: GuardPreset;
|
|
34
|
+
/** 실시간 감시 모드 */
|
|
35
|
+
watch?: boolean;
|
|
36
|
+
/** CI 모드 (에러 시 exit 1) */
|
|
37
|
+
ci?: boolean;
|
|
38
|
+
/** 출력 형식: console, agent, json */
|
|
39
|
+
format?: OutputFormat;
|
|
40
|
+
/** 조용히 (요약만 출력) */
|
|
41
|
+
quiet?: boolean;
|
|
42
|
+
/** 소스 디렉토리 */
|
|
43
|
+
srcDir?: string;
|
|
44
|
+
/** 프리셋 목록 출력 */
|
|
45
|
+
listPresets?: boolean;
|
|
46
|
+
/** 리포트 파일 출력 */
|
|
47
|
+
output?: string;
|
|
48
|
+
/** 리포트 형식: json, markdown, html */
|
|
49
|
+
reportFormat?: "json" | "markdown" | "html";
|
|
50
|
+
/** 통계 저장 (트렌드 분석용) */
|
|
51
|
+
saveStats?: boolean;
|
|
52
|
+
/** 트렌드 분석 표시 */
|
|
53
|
+
showTrend?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function inferReportFormat(output?: string): "json" | "markdown" | "html" | undefined {
|
|
57
|
+
if (!output) return undefined;
|
|
58
|
+
const ext = path.extname(output).toLowerCase();
|
|
59
|
+
if (ext === ".json") return "json";
|
|
60
|
+
if (ext === ".html" || ext === ".htm") return "html";
|
|
61
|
+
if (ext === ".md" || ext === ".markdown") return "markdown";
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
|
|
66
|
+
const rootDir = resolveFromCwd(".");
|
|
67
|
+
const {
|
|
68
|
+
watch = false,
|
|
69
|
+
ci = process.env.CI === "true",
|
|
70
|
+
format,
|
|
71
|
+
quiet = false,
|
|
72
|
+
listPresets: showPresets = false,
|
|
73
|
+
output,
|
|
74
|
+
reportFormat = inferReportFormat(options.output) ?? "markdown",
|
|
75
|
+
saveStats = false,
|
|
76
|
+
showTrend = false,
|
|
77
|
+
} = options;
|
|
78
|
+
const resolvedFormat = resolveOutputFormat(format);
|
|
79
|
+
const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
|
|
80
|
+
|
|
81
|
+
// 프리셋 목록 출력
|
|
82
|
+
if (showPresets) {
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log("🛡️ Mandu Guard - Available Presets");
|
|
85
|
+
console.log("");
|
|
86
|
+
|
|
87
|
+
const presets = listPresets();
|
|
88
|
+
for (const p of presets) {
|
|
89
|
+
const presetDef = getPreset(p.name);
|
|
90
|
+
console.log(` ${p.name === "fsd" ? "✨ " : " "}${p.name}`);
|
|
91
|
+
console.log(` ${p.description}`);
|
|
92
|
+
console.log(` Layers: ${presetDef.hierarchy.join(" → ")}`);
|
|
93
|
+
console.log("");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log("Usage: set guard.preset in mandu.config to choose a preset");
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fileConfig = await validateAndReport(rootDir);
|
|
101
|
+
if (!fileConfig) return false;
|
|
102
|
+
const guardConfigFromFile = fileConfig.guard ?? {};
|
|
103
|
+
|
|
104
|
+
const preset = options.preset ?? guardConfigFromFile.preset ?? "mandu";
|
|
105
|
+
const srcDir = options.srcDir ?? guardConfigFromFile.srcDir ?? "src";
|
|
106
|
+
|
|
107
|
+
if (resolvedFormat === "console") {
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log("🛡️ Mandu Guard - Architecture Checker");
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(`📋 Preset: ${preset}`);
|
|
112
|
+
console.log(`📂 Source: ${srcDir}/`);
|
|
113
|
+
console.log(`🔧 Mode: ${watch ? "Watch" : "Check"}`);
|
|
114
|
+
console.log("");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Guard 설정
|
|
118
|
+
const guardConfig: GuardConfig = {
|
|
119
|
+
preset,
|
|
120
|
+
srcDir,
|
|
121
|
+
realtime: watch,
|
|
122
|
+
realtimeOutput: resolvedFormat,
|
|
123
|
+
exclude: guardConfigFromFile.exclude,
|
|
124
|
+
fsRoutes: enableFsRoutes
|
|
125
|
+
? {
|
|
126
|
+
noPageToPage: true,
|
|
127
|
+
pageCanImport: [
|
|
128
|
+
"client/pages",
|
|
129
|
+
"client/widgets",
|
|
130
|
+
"client/features",
|
|
131
|
+
"client/entities",
|
|
132
|
+
"client/shared",
|
|
133
|
+
"shared/contracts",
|
|
134
|
+
"shared/types",
|
|
135
|
+
"shared/utils/client",
|
|
136
|
+
],
|
|
137
|
+
layoutCanImport: [
|
|
138
|
+
"client/app",
|
|
139
|
+
"client/widgets",
|
|
140
|
+
"client/shared",
|
|
141
|
+
"shared/contracts",
|
|
142
|
+
"shared/types",
|
|
143
|
+
"shared/utils/client",
|
|
144
|
+
],
|
|
145
|
+
routeCanImport: [
|
|
146
|
+
"server/api",
|
|
147
|
+
"server/application",
|
|
148
|
+
"server/domain",
|
|
149
|
+
"server/infra",
|
|
150
|
+
"server/core",
|
|
151
|
+
"shared/contracts",
|
|
152
|
+
"shared/schema",
|
|
153
|
+
"shared/types",
|
|
154
|
+
"shared/utils/client",
|
|
155
|
+
"shared/utils/server",
|
|
156
|
+
"shared/env",
|
|
157
|
+
],
|
|
158
|
+
}
|
|
159
|
+
: undefined,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// 실시간 감시 모드
|
|
163
|
+
if (watch) {
|
|
164
|
+
if (resolvedFormat === "console") {
|
|
165
|
+
console.log("👁️ Watching for architecture violations...");
|
|
166
|
+
console.log(" Press Ctrl+C to stop\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const watcher = createGuardWatcher({
|
|
170
|
+
config: guardConfig,
|
|
171
|
+
rootDir,
|
|
172
|
+
onViolation: (violation) => {
|
|
173
|
+
// 실시간 위반 출력은 watcher 내부에서 처리됨
|
|
174
|
+
},
|
|
175
|
+
onFileAnalyzed: (analysis, violations) => {
|
|
176
|
+
if (resolvedFormat === "console" && violations.length > 0 && !quiet) {
|
|
177
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
178
|
+
console.log(`[${timestamp}] ${analysis.filePath}: ${violations.length} violation(s)`);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
watcher.start();
|
|
184
|
+
|
|
185
|
+
// Ctrl+C 핸들링
|
|
186
|
+
process.on("SIGINT", () => {
|
|
187
|
+
if (resolvedFormat === "console") {
|
|
188
|
+
console.log("\n🛑 Guard stopped");
|
|
189
|
+
}
|
|
190
|
+
watcher.close();
|
|
191
|
+
process.exit(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 계속 실행
|
|
195
|
+
return new Promise(() => {});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 일회성 검사 모드
|
|
199
|
+
if (resolvedFormat === "console" && !quiet) {
|
|
200
|
+
console.log("🔍 Scanning for architecture violations...\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const report = await checkDirectory(guardConfig, rootDir);
|
|
204
|
+
const presetDef = getPreset(preset);
|
|
205
|
+
|
|
206
|
+
// 출력 형식에 따른 리포트 출력
|
|
207
|
+
switch (resolvedFormat) {
|
|
208
|
+
case "json":
|
|
209
|
+
console.log(formatReportAsAgentJSON(report, preset));
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case "agent":
|
|
213
|
+
console.log(formatReportForAgent(report, preset));
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case "console":
|
|
217
|
+
default:
|
|
218
|
+
if (quiet) {
|
|
219
|
+
// 요약만 출력
|
|
220
|
+
console.log(`Files analyzed: ${report.filesAnalyzed}`);
|
|
221
|
+
console.log(`Violations: ${report.totalViolations}`);
|
|
222
|
+
console.log(` Errors: ${report.bySeverity.error}`);
|
|
223
|
+
console.log(` Warnings: ${report.bySeverity.warn}`);
|
|
224
|
+
console.log(` Info: ${report.bySeverity.info}`);
|
|
225
|
+
} else {
|
|
226
|
+
printReport(report, presetDef.hierarchy);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 통계 저장
|
|
232
|
+
if (saveStats) {
|
|
233
|
+
const scanRecord = createScanRecord(report, preset);
|
|
234
|
+
await addScanRecord(rootDir, scanRecord);
|
|
235
|
+
console.log("📊 Statistics saved to .mandu/guard-stats.json");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 트렌드 분석
|
|
239
|
+
let trend = null;
|
|
240
|
+
let layerStats = null;
|
|
241
|
+
|
|
242
|
+
if (showTrend) {
|
|
243
|
+
const store = await loadStatistics(rootDir);
|
|
244
|
+
trend = analyzeTrend(store.records, 7);
|
|
245
|
+
layerStats = calculateLayerStatistics(report.violations, presetDef.hierarchy);
|
|
246
|
+
|
|
247
|
+
if (trend) {
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log("📈 Trend Analysis (7 days):");
|
|
250
|
+
const trendEmoji = trend.trend === "improving" ? "📉" : trend.trend === "degrading" ? "📈" : "➡️";
|
|
251
|
+
console.log(` Status: ${trendEmoji} ${trend.trend.toUpperCase()}`);
|
|
252
|
+
console.log(` Change: ${trend.violationDelta >= 0 ? "+" : ""}${trend.violationDelta} (${trend.violationChangePercent >= 0 ? "+" : ""}${trend.violationChangePercent}%)`);
|
|
253
|
+
|
|
254
|
+
if (trend.recommendations.length > 0) {
|
|
255
|
+
console.log(" 💡 Recommendations:");
|
|
256
|
+
for (const rec of trend.recommendations) {
|
|
257
|
+
console.log(` - ${rec}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 리포트 파일 출력
|
|
264
|
+
if (output) {
|
|
265
|
+
let reportContent: string;
|
|
266
|
+
|
|
267
|
+
switch (reportFormat) {
|
|
268
|
+
case "json":
|
|
269
|
+
reportContent = formatReportAsAgentJSON(report, preset);
|
|
270
|
+
break;
|
|
271
|
+
case "html":
|
|
272
|
+
reportContent = generateHTMLReport(report, trend, layerStats ?? undefined);
|
|
273
|
+
break;
|
|
274
|
+
case "markdown":
|
|
275
|
+
default:
|
|
276
|
+
reportContent = generateGuardMarkdownReport(report, trend, layerStats ?? undefined);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await writeFile(output, reportContent);
|
|
281
|
+
console.log(`\n📄 Report saved to ${output}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// CI 모드에서 에러가 있으면 실패
|
|
285
|
+
const hasErrors = report.bySeverity.error > 0;
|
|
286
|
+
const hasWarnings = report.bySeverity.warn > 0;
|
|
287
|
+
|
|
288
|
+
if (report.totalViolations === 0) {
|
|
289
|
+
console.log("\n✅ Architecture check passed");
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (hasErrors || (ci && hasWarnings)) {
|
|
294
|
+
const reason = hasErrors
|
|
295
|
+
? `${report.bySeverity.error} error(s)`
|
|
296
|
+
: `${report.bySeverity.warn} warning(s)`;
|
|
297
|
+
console.log(`\n❌ Architecture check failed: ${reason}`);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(`\n⚠️ ${report.totalViolations} issue(s) found`);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
@@ -13,13 +13,13 @@ export interface GuardCheckOptions {
|
|
|
13
13
|
autoCorrect?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export async function guardCheck(options: GuardCheckOptions = {}): Promise<boolean> {
|
|
17
|
-
const { autoCorrect = true } = options;
|
|
16
|
+
export async function guardCheck(options: GuardCheckOptions = {}): Promise<boolean> {
|
|
17
|
+
const { autoCorrect = true } = options;
|
|
18
18
|
|
|
19
19
|
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
20
20
|
const rootDir = getRootDir();
|
|
21
21
|
|
|
22
|
-
console.log(`🥟 Mandu Guard (Legacy Spec)`);
|
|
22
|
+
console.log(`🥟 Mandu Guard (Legacy Spec)`);
|
|
23
23
|
console.log(`📄 Spec 파일: ${specPath}`);
|
|
24
24
|
console.log(`🔧 Auto-correct: ${autoCorrect ? "ON" : "OFF"}\n`);
|
|
25
25
|
|