@mandujs/cli 0.9.12 → 0.9.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.9.12",
3
+ "version": "0.9.17",
4
4
  "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "0.9.10"
35
+ "@mandujs/core": "0.9.37"
36
36
  },
37
37
  "engines": {
38
38
  "bun": ">=1.0.0"
@@ -1,13 +1,21 @@
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
20
  import { resolveFromCwd } from "../util/fs";
13
21
  import path from "path";
@@ -16,13 +24,18 @@ export interface DevOptions {
16
24
  port?: number;
17
25
  /** HMR 비활성화 */
18
26
  noHmr?: boolean;
27
+ /** FS Routes 비활성화 (레거시 모드) */
28
+ legacy?: boolean;
29
+ /** Architecture Guard 활성화 */
30
+ guard?: boolean;
31
+ /** Guard 프리셋 */
32
+ guardPreset?: GuardPreset;
19
33
  }
20
34
 
21
35
  export async function dev(options: DevOptions = {}): Promise<void> {
22
- const specPath = resolveFromCwd("spec/routes.manifest.json");
23
36
  const rootDir = resolveFromCwd(".");
24
37
 
25
- console.log(`🥟 Mandu Dev Server`);
38
+ console.log(`🥟 Mandu Dev Server (FS Routes)`);
26
39
 
27
40
  // .env 파일 로드
28
41
  const envResult = await loadEnv({
@@ -34,50 +47,93 @@ export async function dev(options: DevOptions = {}): Promise<void> {
34
47
  console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
35
48
  }
36
49
 
37
- console.log(`📄 Spec 파일: ${specPath}\n`);
50
+ // FS Routes 스캔
51
+ console.log(`📂 app/ 폴더 스캔 중...`);
38
52
 
39
- const result = await loadManifest(specPath);
53
+ const result = await generateManifest(rootDir, {
54
+ skipLegacy: true,
55
+ });
40
56
 
41
- if (!result.success || !result.data) {
42
- console.error("❌ Spec 로드 실패:");
43
- result.errors?.forEach((e) => console.error(` - ${e}`));
57
+ if (result.manifest.routes.length === 0) {
58
+ console.log("");
59
+ console.log("📭 라우트가 없습니다.");
60
+ console.log("");
61
+ console.log("💡 app/ 폴더에 page.tsx 파일을 생성하세요:");
62
+ console.log("");
63
+ console.log(" app/page.tsx → /");
64
+ console.log(" app/blog/page.tsx → /blog");
65
+ console.log(" app/api/users/route.ts → /api/users");
66
+ console.log("");
44
67
  process.exit(1);
45
68
  }
46
69
 
47
- const manifest = result.data;
48
- console.log(`✅ Spec 로드 완료: ${manifest.routes.length}개 라우트`);
49
-
50
- // 핸들러 등록
51
- for (const route of manifest.routes) {
52
- if (route.kind === "api") {
53
- const modulePath = path.resolve(rootDir, route.module);
54
- try {
55
- const module = await import(modulePath);
56
- registerApiHandler(route.id, module.default || module.handler);
57
- console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
58
- } catch (error) {
59
- console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
60
- }
61
- } else if (route.kind === "page" && route.componentModule) {
62
- const componentPath = path.resolve(rootDir, route.componentModule);
63
- const isIsland = needsHydration(route);
64
-
65
- // slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
66
- if (route.slotModule) {
67
- registerPageHandler(route.id, async () => {
68
- const module = await import(componentPath);
69
- // module.default = { component, filling }
70
- return module.default;
71
- });
72
- console.log(` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}`);
73
- } else {
74
- // slotModule이 없으면 기존 PageLoader 사용
75
- registerPageLoader(route.id, () => import(componentPath));
76
- console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}`);
70
+ let manifest = result.manifest;
71
+ console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
72
+
73
+ // Layout 경로 추적 (중복 등록 방지)
74
+ const registeredLayouts = new Set<string>();
75
+
76
+ // 핸들러 등록 함수
77
+ const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
78
+ // 리로드 레이아웃 캐시 클리어
79
+ if (isReload) {
80
+ registeredLayouts.clear();
81
+ }
82
+
83
+ for (const route of manifest.routes) {
84
+ if (route.kind === "api") {
85
+ const modulePath = path.resolve(rootDir, route.module);
86
+ try {
87
+ // 캐시 무효화 (HMR용)
88
+ delete require.cache[modulePath];
89
+ const module = await import(modulePath);
90
+ registerApiHandler(route.id, module.default || module.handler || module);
91
+ console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
92
+ } catch (error) {
93
+ console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
94
+ }
95
+ } else if (route.kind === "page" && route.componentModule) {
96
+ const componentPath = path.resolve(rootDir, route.componentModule);
97
+ const isIsland = needsHydration(route);
98
+ const hasLayout = route.layoutChain && route.layoutChain.length > 0;
99
+
100
+ // Layout 로더 등록
101
+ if (route.layoutChain) {
102
+ for (const layoutPath of route.layoutChain) {
103
+ if (!registeredLayouts.has(layoutPath)) {
104
+ const absLayoutPath = path.resolve(rootDir, layoutPath);
105
+ registerLayoutLoader(layoutPath, async () => {
106
+ // 캐시 무효화 (HMR용)
107
+ delete require.cache[absLayoutPath];
108
+ return import(absLayoutPath);
109
+ });
110
+ registeredLayouts.add(layoutPath);
111
+ console.log(` 🎨 Layout: ${layoutPath}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ // slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
117
+ if (route.slotModule) {
118
+ registerPageHandler(route.id, async () => {
119
+ delete require.cache[componentPath];
120
+ const module = await import(componentPath);
121
+ return module.default;
122
+ });
123
+ console.log(` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
124
+ } else {
125
+ registerPageLoader(route.id, () => {
126
+ delete require.cache[componentPath];
127
+ return import(componentPath);
128
+ });
129
+ console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
130
+ }
77
131
  }
78
132
  }
79
- }
133
+ };
80
134
 
135
+ // 초기 핸들러 등록
136
+ await registerHandlers(manifest);
81
137
  console.log("");
82
138
 
83
139
  const port = options.port || Number(process.env.PORT) || 3000;
@@ -98,28 +154,28 @@ export async function dev(options: DevOptions = {}): Promise<void> {
98
154
  devBundler = await startDevBundler({
99
155
  rootDir,
100
156
  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: {
157
+ onRebuild: (result) => {
158
+ if (result.success) {
159
+ if (result.routeId === "*") {
160
+ hmrServer?.broadcast({
161
+ type: "reload",
162
+ data: {
163
+ timestamp: Date.now(),
164
+ },
165
+ });
166
+ } else {
167
+ hmrServer?.broadcast({
168
+ type: "island-update",
169
+ data: {
170
+ routeId: result.routeId,
171
+ timestamp: Date.now(),
172
+ },
173
+ });
174
+ }
175
+ } else {
176
+ hmrServer?.broadcast({
177
+ type: "error",
178
+ data: {
123
179
  routeId: result.routeId,
124
180
  message: result.error,
125
181
  },
@@ -147,12 +203,80 @@ export async function dev(options: DevOptions = {}): Promise<void> {
147
203
  bundleManifest: devBundler?.initialBuild.manifest,
148
204
  });
149
205
 
206
+ // Architecture Guard 실시간 감시 (선택적)
207
+ let archGuardWatcher: ReturnType<typeof createGuardWatcher> | null = null;
208
+
209
+ if (options.guard) {
210
+ const guardPreset = options.guardPreset || "mandu";
211
+ const guardConfig: GuardConfig = {
212
+ preset: guardPreset,
213
+ srcDir: "src",
214
+ realtime: true,
215
+ };
216
+
217
+ console.log(`🛡️ Architecture Guard 활성화 (${guardPreset})`);
218
+
219
+ archGuardWatcher = createGuardWatcher({
220
+ config: guardConfig,
221
+ rootDir,
222
+ onViolation: (violation) => {
223
+ // 실시간 경고는 watcher 내부에서 처리
224
+ },
225
+ onFileAnalyzed: (analysis, violations) => {
226
+ if (violations.length > 0) {
227
+ // HMR 에러로 브로드캐스트
228
+ hmrServer?.broadcast({
229
+ type: "guard-violation",
230
+ data: {
231
+ file: analysis.filePath,
232
+ violations: violations.map((v) => ({
233
+ line: v.line,
234
+ message: `${v.fromLayer} → ${v.toLayer}: ${v.ruleDescription}`,
235
+ })),
236
+ },
237
+ });
238
+ }
239
+ },
240
+ });
241
+
242
+ archGuardWatcher.start();
243
+ }
244
+
245
+ // FS Routes 실시간 감시
246
+ const routesWatcher = await watchFSRoutes(rootDir, {
247
+ skipLegacy: true,
248
+ onChange: async (result) => {
249
+ const timestamp = new Date().toLocaleTimeString();
250
+ console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
251
+
252
+ // 레지스트리 클리어 (layout 캐시 포함)
253
+ clearDefaultRegistry();
254
+
255
+ // 새 매니페스트로 서버 업데이트
256
+ manifest = result.manifest;
257
+ console.log(` 📋 라우트: ${manifest.routes.length}개`);
258
+
259
+ // 라우트 재등록 (isReload = true)
260
+ await registerHandlers(manifest, true);
261
+
262
+ // HMR 브로드캐스트 (전체 리로드)
263
+ if (hmrServer) {
264
+ hmrServer.broadcast({
265
+ type: "reload",
266
+ data: { timestamp: Date.now() },
267
+ });
268
+ }
269
+ },
270
+ });
271
+
150
272
  // 정리 함수
151
273
  const cleanup = () => {
152
274
  console.log("\n🛑 서버 종료 중...");
153
275
  server.stop();
154
276
  devBundler?.close();
155
277
  hmrServer?.close();
278
+ routesWatcher.close();
279
+ archGuardWatcher?.close();
156
280
  process.exit(0);
157
281
  };
158
282
 
@@ -0,0 +1,238 @@
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
+ generateMarkdownReport,
21
+ generateHTMLReport,
22
+ type GuardConfig,
23
+ type GuardPreset,
24
+ } from "@mandujs/core";
25
+ import { writeFile } from "fs/promises";
26
+ import { resolveFromCwd } from "../util/fs";
27
+
28
+ export interface GuardArchOptions {
29
+ /** 프리셋 이름 */
30
+ preset?: GuardPreset;
31
+ /** 실시간 감시 모드 */
32
+ watch?: boolean;
33
+ /** CI 모드 (에러 시 exit 1) */
34
+ ci?: boolean;
35
+ /** 출력 형식: console, agent, json */
36
+ format?: "console" | "agent" | "json";
37
+ /** 조용히 (요약만 출력) */
38
+ quiet?: boolean;
39
+ /** 소스 디렉토리 */
40
+ srcDir?: string;
41
+ /** 프리셋 목록 출력 */
42
+ listPresets?: boolean;
43
+ /** 리포트 파일 출력 */
44
+ output?: string;
45
+ /** 리포트 형식: json, markdown, html */
46
+ reportFormat?: "json" | "markdown" | "html";
47
+ /** 통계 저장 (트렌드 분석용) */
48
+ saveStats?: boolean;
49
+ /** 트렌드 분석 표시 */
50
+ showTrend?: boolean;
51
+ }
52
+
53
+ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
54
+ const {
55
+ preset = "mandu",
56
+ watch = false,
57
+ ci = false,
58
+ format = "console",
59
+ quiet = false,
60
+ srcDir = "src",
61
+ listPresets: showPresets = false,
62
+ output,
63
+ reportFormat = "markdown",
64
+ saveStats = false,
65
+ showTrend = false,
66
+ } = options;
67
+
68
+ const rootDir = resolveFromCwd(".");
69
+
70
+ // 프리셋 목록 출력
71
+ if (showPresets) {
72
+ console.log("");
73
+ console.log("🛡️ Mandu Guard - Available Presets");
74
+ console.log("");
75
+
76
+ const presets = listPresets();
77
+ for (const p of presets) {
78
+ const presetDef = getPreset(p.name);
79
+ console.log(` ${p.name === "fsd" ? "✨ " : " "}${p.name}`);
80
+ console.log(` ${p.description}`);
81
+ console.log(` Layers: ${presetDef.hierarchy.join(" → ")}`);
82
+ console.log("");
83
+ }
84
+
85
+ console.log("Usage: bunx mandu guard arch --preset <name>");
86
+ return true;
87
+ }
88
+
89
+ console.log("");
90
+ console.log("🛡️ Mandu Guard - Architecture Checker");
91
+ console.log("");
92
+ console.log(`📋 Preset: ${preset}`);
93
+ console.log(`📂 Source: ${srcDir}/`);
94
+ console.log(`🔧 Mode: ${watch ? "Watch" : "Check"}`);
95
+ console.log("");
96
+
97
+ // Guard 설정
98
+ const config: GuardConfig = {
99
+ preset,
100
+ srcDir,
101
+ realtime: watch,
102
+ };
103
+
104
+ // 실시간 감시 모드
105
+ if (watch) {
106
+ console.log("👁️ Watching for architecture violations...");
107
+ console.log(" Press Ctrl+C to stop\n");
108
+
109
+ const watcher = createGuardWatcher({
110
+ config,
111
+ rootDir,
112
+ onViolation: (violation) => {
113
+ // 실시간 위반 출력은 watcher 내부에서 처리됨
114
+ },
115
+ onFileAnalyzed: (analysis, violations) => {
116
+ if (violations.length > 0 && !quiet) {
117
+ const timestamp = new Date().toLocaleTimeString();
118
+ console.log(`[${timestamp}] ${analysis.filePath}: ${violations.length} violation(s)`);
119
+ }
120
+ },
121
+ });
122
+
123
+ watcher.start();
124
+
125
+ // Ctrl+C 핸들링
126
+ process.on("SIGINT", () => {
127
+ console.log("\n🛑 Guard stopped");
128
+ watcher.close();
129
+ process.exit(0);
130
+ });
131
+
132
+ // 계속 실행
133
+ return new Promise(() => {});
134
+ }
135
+
136
+ // 일회성 검사 모드
137
+ console.log("🔍 Scanning for architecture violations...\n");
138
+
139
+ const report = await checkDirectory(config, rootDir);
140
+ const presetDef = getPreset(preset);
141
+
142
+ // 출력 형식에 따른 리포트 출력
143
+ switch (format) {
144
+ case "json":
145
+ console.log(formatReportAsAgentJSON(report, preset));
146
+ break;
147
+
148
+ case "agent":
149
+ console.log(formatReportForAgent(report, preset));
150
+ break;
151
+
152
+ case "console":
153
+ default:
154
+ if (quiet) {
155
+ // 요약만 출력
156
+ console.log(`Files analyzed: ${report.filesAnalyzed}`);
157
+ console.log(`Violations: ${report.totalViolations}`);
158
+ console.log(` Errors: ${report.bySeverity.error}`);
159
+ console.log(` Warnings: ${report.bySeverity.warn}`);
160
+ console.log(` Info: ${report.bySeverity.info}`);
161
+ } else {
162
+ printReport(report, presetDef.hierarchy);
163
+ }
164
+ break;
165
+ }
166
+
167
+ // 통계 저장
168
+ if (saveStats) {
169
+ const scanRecord = createScanRecord(report, preset);
170
+ await addScanRecord(rootDir, scanRecord);
171
+ console.log("📊 Statistics saved to .mandu/guard-stats.json");
172
+ }
173
+
174
+ // 트렌드 분석
175
+ let trend = null;
176
+ let layerStats = null;
177
+
178
+ if (showTrend) {
179
+ const store = await loadStatistics(rootDir);
180
+ trend = analyzeTrend(store.records, 7);
181
+ layerStats = calculateLayerStatistics(report.violations, presetDef.hierarchy);
182
+
183
+ if (trend) {
184
+ console.log("");
185
+ console.log("📈 Trend Analysis (7 days):");
186
+ const trendEmoji = trend.trend === "improving" ? "📉" : trend.trend === "degrading" ? "📈" : "➡️";
187
+ console.log(` Status: ${trendEmoji} ${trend.trend.toUpperCase()}`);
188
+ console.log(` Change: ${trend.violationDelta >= 0 ? "+" : ""}${trend.violationDelta} (${trend.violationChangePercent >= 0 ? "+" : ""}${trend.violationChangePercent}%)`);
189
+
190
+ if (trend.recommendations.length > 0) {
191
+ console.log(" 💡 Recommendations:");
192
+ for (const rec of trend.recommendations) {
193
+ console.log(` - ${rec}`);
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // 리포트 파일 출력
200
+ if (output) {
201
+ let reportContent: string;
202
+
203
+ switch (reportFormat) {
204
+ case "json":
205
+ reportContent = formatReportAsAgentJSON(report, preset);
206
+ break;
207
+ case "html":
208
+ reportContent = generateHTMLReport(report, trend, layerStats ?? undefined);
209
+ break;
210
+ case "markdown":
211
+ default:
212
+ reportContent = generateMarkdownReport(report, trend, layerStats ?? undefined);
213
+ break;
214
+ }
215
+
216
+ await writeFile(output, reportContent);
217
+ console.log(`\n📄 Report saved to ${output}`);
218
+ }
219
+
220
+ // CI 모드에서 에러가 있으면 실패
221
+ if (ci && report.bySeverity.error > 0) {
222
+ console.log("\n❌ Architecture check failed");
223
+ return false;
224
+ }
225
+
226
+ if (report.totalViolations === 0) {
227
+ console.log("\n✅ Architecture check passed");
228
+ return true;
229
+ }
230
+
231
+ if (report.bySeverity.error > 0) {
232
+ console.log(`\n⚠️ ${report.bySeverity.error} error(s) found - please fix before continuing`);
233
+ return !ci;
234
+ }
235
+
236
+ console.log(`\n⚠️ ${report.totalViolations} issue(s) found`);
237
+ return true;
238
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * FS Routes CLI Commands
3
+ *
4
+ * 파일 시스템 기반 라우트 관리 명령어
5
+ */
6
+
7
+ import {
8
+ scanRoutes,
9
+ generateManifest,
10
+ formatRoutesForCLI,
11
+ watchFSRoutes,
12
+ type GenerateOptions,
13
+ type FSScannerConfig,
14
+ } from "@mandujs/core";
15
+ import { resolveFromCwd } from "../util/fs";
16
+
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // Types
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ export interface RoutesGenerateOptions {
22
+ /** 출력 파일 경로 */
23
+ output?: string;
24
+ /** 상세 출력 */
25
+ verbose?: boolean;
26
+ }
27
+
28
+ export interface RoutesListOptions {
29
+ /** 상세 출력 */
30
+ verbose?: boolean;
31
+ }
32
+
33
+ export interface RoutesWatchOptions {
34
+ /** 출력 파일 경로 */
35
+ output?: string;
36
+ /** 상세 출력 */
37
+ verbose?: boolean;
38
+ }
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+ // Commands
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+
44
+ /**
45
+ * routes generate - FS Routes 스캔 및 매니페스트 생성
46
+ */
47
+ export async function routesGenerate(options: RoutesGenerateOptions = {}): Promise<boolean> {
48
+ const rootDir = resolveFromCwd(".");
49
+
50
+ console.log("🥟 Mandu FS Routes Generate\n");
51
+
52
+ try {
53
+ const generateOptions: GenerateOptions = {
54
+ outputPath: options.output ?? ".mandu/routes.manifest.json",
55
+ skipLegacy: true, // 레거시 병합 비활성화
56
+ };
57
+
58
+ const result = await generateManifest(rootDir, generateOptions);
59
+
60
+ // 결과 출력
61
+ console.log(`✅ FS Routes 스캔 완료`);
62
+ console.log(` 📋 라우트: ${result.manifest.routes.length}개\n`);
63
+
64
+ // 경고 출력
65
+ if (result.warnings.length > 0) {
66
+ console.log("⚠️ 경고:");
67
+ for (const warning of result.warnings) {
68
+ console.log(` - ${warning}`);
69
+ }
70
+ console.log("");
71
+ }
72
+
73
+ // 라우트 목록 출력
74
+ if (options.verbose) {
75
+ console.log(formatRoutesForCLI(result.manifest));
76
+ console.log("");
77
+ }
78
+
79
+ // 출력 파일 경로
80
+ if (generateOptions.outputPath) {
81
+ console.log(`📁 매니페스트 저장: ${generateOptions.outputPath}`);
82
+ }
83
+
84
+ return true;
85
+ } catch (error) {
86
+ console.error("❌ FS Routes 생성 실패:", error instanceof Error ? error.message : error);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * routes list - 현재 라우트 목록 출력
93
+ */
94
+ export async function routesList(options: RoutesListOptions = {}): Promise<boolean> {
95
+ const rootDir = resolveFromCwd(".");
96
+
97
+ console.log("🥟 Mandu Routes List\n");
98
+
99
+ try {
100
+ const result = await scanRoutes(rootDir);
101
+
102
+ if (result.errors.length > 0) {
103
+ console.log("⚠️ 스캔 경고:");
104
+ for (const error of result.errors) {
105
+ console.log(` - ${error.type}: ${error.message}`);
106
+ }
107
+ console.log("");
108
+ }
109
+
110
+ if (result.routes.length === 0) {
111
+ console.log("📭 라우트가 없습니다.");
112
+ console.log("");
113
+ console.log("💡 app/ 폴더에 page.tsx 또는 route.ts 파일을 생성하세요.");
114
+ console.log("");
115
+ console.log("예시:");
116
+ console.log(" app/page.tsx → /");
117
+ console.log(" app/blog/page.tsx → /blog");
118
+ console.log(" app/api/users/route.ts → /api/users");
119
+ return true;
120
+ }
121
+
122
+ // 라우트 목록 출력
123
+ console.log(`📋 라우트 (${result.routes.length}개)`);
124
+ console.log("─".repeat(70));
125
+
126
+ for (const route of result.routes) {
127
+ const icon = route.kind === "page" ? "📄" : "📡";
128
+ const hydration = route.clientModule ? " 🏝️" : "";
129
+ const pattern = route.pattern.padEnd(35);
130
+ const id = route.id;
131
+
132
+ console.log(`${icon} ${pattern} → ${id}${hydration}`);
133
+
134
+ if (options.verbose) {
135
+ console.log(` 📁 ${route.sourceFile}`);
136
+ if (route.clientModule) {
137
+ console.log(` 🏝️ ${route.clientModule}`);
138
+ }
139
+ if (route.layoutChain.length > 0) {
140
+ console.log(` 📐 layouts: ${route.layoutChain.join(" → ")}`);
141
+ }
142
+ }
143
+ }
144
+
145
+ console.log("");
146
+
147
+ // 통계
148
+ console.log("📊 통계");
149
+ console.log(` 페이지: ${result.stats.pageCount}개`);
150
+ console.log(` API: ${result.stats.apiCount}개`);
151
+ console.log(` 레이아웃: ${result.stats.layoutCount}개`);
152
+ console.log(` Island: ${result.stats.islandCount}개`);
153
+ console.log(` 스캔 시간: ${result.stats.scanTime}ms`);
154
+
155
+ return true;
156
+ } catch (error) {
157
+ console.error("❌ 라우트 목록 조회 실패:", error instanceof Error ? error.message : error);
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * routes watch - 실시간 라우트 감시
164
+ */
165
+ export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boolean> {
166
+ const rootDir = resolveFromCwd(".");
167
+
168
+ console.log("🥟 Mandu FS Routes Watch\n");
169
+ console.log("👀 라우트 변경 감시 중... (Ctrl+C로 종료)\n");
170
+
171
+ try {
172
+ // 초기 스캔
173
+ const initialResult = await generateManifest(rootDir, {
174
+ outputPath: options.output ?? ".mandu/routes.manifest.json",
175
+ });
176
+
177
+ console.log(`✅ 초기 스캔: ${initialResult.manifest.routes.length}개 라우트\n`);
178
+
179
+ // 감시 시작
180
+ const watcher = await watchFSRoutes(rootDir, {
181
+ outputPath: options.output ?? ".mandu/routes.manifest.json",
182
+ onChange: (result) => {
183
+ const timestamp = new Date().toLocaleTimeString();
184
+ console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
185
+ console.log(` 📋 총 라우트: ${result.manifest.routes.length}개`);
186
+
187
+ if (result.warnings.length > 0) {
188
+ for (const warning of result.warnings) {
189
+ console.log(` ⚠️ ${warning}`);
190
+ }
191
+ }
192
+
193
+ if (options.verbose) {
194
+ console.log("");
195
+ console.log(formatRoutesForCLI(result.manifest));
196
+ }
197
+ },
198
+ });
199
+
200
+ // 종료 시그널 처리
201
+ const cleanup = () => {
202
+ console.log("\n\n🛑 감시 종료");
203
+ watcher.close();
204
+ process.exit(0);
205
+ };
206
+
207
+ process.on("SIGINT", cleanup);
208
+ process.on("SIGTERM", cleanup);
209
+
210
+ // 무한 대기
211
+ await new Promise(() => {});
212
+
213
+ return true;
214
+ } catch (error) {
215
+ console.error("❌ 라우트 감시 실패:", error instanceof Error ? error.message : error);
216
+ return false;
217
+ }
218
+ }
package/src/main.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { specUpsert } from "./commands/spec-upsert";
4
4
  import { generateApply } from "./commands/generate-apply";
5
5
  import { guardCheck } from "./commands/guard-check";
6
+ import { guardArch } from "./commands/guard-arch";
6
7
  import { dev } from "./commands/dev";
7
8
  import { init } from "./commands/init";
8
9
  import { build } from "./commands/build";
@@ -19,6 +20,7 @@ import {
19
20
  import { doctor } from "./commands/doctor";
20
21
  import { watch } from "./commands/watch";
21
22
  import { brainSetup, brainStatus } from "./commands/brain";
23
+ import { routesGenerate, routesList, routesWatch } from "./commands/routes";
22
24
 
23
25
  const HELP_TEXT = `
24
26
  🥟 Mandu CLI - Agent-Native Fullstack Framework
@@ -27,11 +29,20 @@ Usage: bunx mandu <command> [options]
27
29
 
28
30
  Commands:
29
31
  init 새 프로젝트 생성
30
- spec-upsert Spec 파일 검증lock 갱신
31
- generate Spec에서 코드 생성
32
- guard Guard 규칙 검사
32
+ routes generate FS Routes 스캔 매니페스트 생성
33
+ routes list 현재 라우트 목록 출력
34
+ routes watch 실시간 라우트 감시
35
+ dev 개발 서버 실행 (FS Routes 자동 적용)
36
+ dev --guard Guard 실시간 감시와 함께 개발 서버 실행
33
37
  build 클라이언트 번들 빌드 (Hydration)
34
- dev 개발 서버 실행
38
+ guard Guard 규칙 검사 (레거시 Spec 기반)
39
+ guard arch 아키텍처 위반 검사 (FSD/Clean/Hexagonal)
40
+ guard arch --watch 실시간 아키텍처 감시
41
+ guard arch --list-presets 사용 가능한 프리셋 목록
42
+ guard arch --output report.md 리포트 파일 생성
43
+ guard arch --show-trend 트렌드 분석 표시
44
+ spec-upsert Spec 파일 검증 및 lock 갱신 (레거시)
45
+ generate Spec에서 코드 생성 (레거시)
35
46
 
36
47
  doctor Guard 실패 분석 + 패치 제안 (Brain)
37
48
  watch 실시간 파일 감시 - 경고만 (Brain)
@@ -56,15 +67,23 @@ Options:
56
67
  --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
57
68
  --file <path> spec-upsert 시 사용할 spec 파일 경로
58
69
  --port <port> dev/openapi serve 포트 (기본: 3000/8080)
70
+ --guard dev 시 Architecture Guard 실시간 감시 활성화
71
+ --guard-preset <p> dev --guard 시 프리셋 (기본: mandu)
59
72
  --no-auto-correct guard 시 자동 수정 비활성화
73
+ --preset <name> guard arch 프리셋 (기본: mandu) - fsd, clean, hexagonal, atomic 선택 가능
74
+ --ci guard arch CI 모드 (에러 시 exit 1)
75
+ --quiet guard arch 요약만 출력
76
+ --report-format guard arch 리포트 형식: json, markdown, html
77
+ --save-stats guard arch 통계 저장 (트렌드 분석용)
78
+ --show-trend guard arch 트렌드 분석 표시
60
79
  --minify build 시 코드 압축
61
80
  --sourcemap build 시 소스맵 생성
62
- --watch build 파일 감시 모드
81
+ --watch build/guard arch 파일 감시 모드
63
82
  --message <msg> change begin 시 설명 메시지
64
83
  --id <id> change rollback 시 특정 변경 ID
65
84
  --keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
66
85
  --output <path> openapi/doctor 출력 경로
67
- --format <fmt> doctor 출력 형식: console, json, markdown (기본: console)
86
+ --format <fmt> doctor/guard 출력 형식: console, json, agent
68
87
  --no-llm doctor에서 LLM 사용 안 함 (템플릿 모드)
69
88
  --model <name> brain setup 시 모델 이름 (기본: llama3.2)
70
89
  --url <url> brain setup 시 Ollama URL
@@ -73,26 +92,24 @@ Options:
73
92
 
74
93
  Examples:
75
94
  bunx mandu init --name my-app
76
- bunx mandu spec-upsert
77
- bunx mandu generate
78
- bunx mandu guard
79
- bunx mandu build --minify
80
- bunx mandu build --watch
95
+ bunx mandu routes list
96
+ bunx mandu routes generate
81
97
  bunx mandu dev --port 3000
98
+ bunx mandu build --minify
99
+ bunx mandu guard
100
+ bunx mandu guard arch --preset fsd
101
+ bunx mandu guard arch --watch
102
+ bunx mandu guard arch --ci --format json
82
103
  bunx mandu doctor
83
- bunx mandu doctor --format markdown --output report.md
84
- bunx mandu watch
85
104
  bunx mandu brain setup --model codellama
86
- bunx mandu brain status
87
105
  bunx mandu contract create users
88
- bunx mandu contract validate --verbose
89
106
  bunx mandu openapi generate --output docs/api.json
90
- bunx mandu openapi serve --port 8080
91
107
  bunx mandu change begin --message "Add new route"
92
- bunx mandu change commit
93
- bunx mandu change rollback
94
108
 
95
- Workflow:
109
+ FS Routes Workflow (권장):
110
+ 1. init → 2. app/ 폴더에 page.tsx 생성 → 3. dev → 4. build
111
+
112
+ Legacy Workflow:
96
113
  1. init → 2. spec-upsert → 3. generate → 4. build → 5. guard → 6. dev
97
114
 
98
115
  Contract-first Workflow:
@@ -174,11 +191,32 @@ async function main(): Promise<void> {
174
191
  success = await generateApply();
175
192
  break;
176
193
 
177
- case "guard":
178
- success = await guardCheck({
179
- autoCorrect: options["no-auto-correct"] !== "true",
180
- });
194
+ case "guard": {
195
+ const subCommand = args[1];
196
+ switch (subCommand) {
197
+ case "arch":
198
+ success = await guardArch({
199
+ preset: (options.preset as any) || "fsd",
200
+ watch: options.watch === "true",
201
+ ci: options.ci === "true",
202
+ format: (options.format as any) || "console",
203
+ quiet: options.quiet === "true",
204
+ srcDir: options["src-dir"],
205
+ listPresets: options["list-presets"] === "true",
206
+ output: options.output,
207
+ reportFormat: (options["report-format"] as any) || "markdown",
208
+ saveStats: options["save-stats"] === "true",
209
+ showTrend: options["show-trend"] === "true",
210
+ });
211
+ break;
212
+ default:
213
+ // 기본값: 레거시 guard-check
214
+ success = await guardCheck({
215
+ autoCorrect: options["no-auto-correct"] !== "true",
216
+ });
217
+ }
181
218
  break;
219
+ }
182
220
 
183
221
  case "build":
184
222
  success = await build({
@@ -189,8 +227,47 @@ async function main(): Promise<void> {
189
227
  break;
190
228
 
191
229
  case "dev":
192
- await dev({ port: parsePort(options.port) });
230
+ await dev({
231
+ port: parsePort(options.port),
232
+ guard: options.guard === "true",
233
+ guardPreset: options["guard-preset"] as any,
234
+ });
235
+ break;
236
+
237
+ case "routes": {
238
+ const subCommand = args[1];
239
+ switch (subCommand) {
240
+ case "generate":
241
+ success = await routesGenerate({
242
+ output: options.output,
243
+ verbose: options.verbose === "true",
244
+ });
245
+ break;
246
+ case "list":
247
+ success = await routesList({
248
+ verbose: options.verbose === "true",
249
+ });
250
+ break;
251
+ case "watch":
252
+ success = await routesWatch({
253
+ output: options.output,
254
+ verbose: options.verbose === "true",
255
+ });
256
+ break;
257
+ default:
258
+ // 기본값: list
259
+ if (!subCommand) {
260
+ success = await routesList({
261
+ verbose: options.verbose === "true",
262
+ });
263
+ } else {
264
+ console.error(`❌ Unknown routes subcommand: ${subCommand}`);
265
+ console.log("\nUsage: bunx mandu routes <generate|list|watch>");
266
+ process.exit(1);
267
+ }
268
+ }
193
269
  break;
270
+ }
194
271
 
195
272
  case "contract": {
196
273
  const subCommand = args[1];