@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 +2 -2
- package/src/commands/dev.ts +185 -61
- package/src/commands/guard-arch.ts +238 -0
- package/src/commands/routes.ts +218 -0
- package/src/main.ts +101 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/cli",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
35
|
+
"@mandujs/core": "0.9.37"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"bun": ">=1.0.0"
|
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
-
|
|
50
|
+
// FS Routes 스캔
|
|
51
|
+
console.log(`📂 app/ 폴더 스캔 중...`);
|
|
38
52
|
|
|
39
|
-
const result = await
|
|
53
|
+
const result = await generateManifest(rootDir, {
|
|
54
|
+
skipLegacy: true,
|
|
55
|
+
});
|
|
40
56
|
|
|
41
|
-
if (
|
|
42
|
-
console.
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
routes generate FS Routes 스캔 및 매니페스트 생성
|
|
33
|
+
routes list 현재 라우트 목록 출력
|
|
34
|
+
routes watch 실시간 라우트 감시
|
|
35
|
+
dev 개발 서버 실행 (FS Routes 자동 적용)
|
|
36
|
+
dev --guard Guard 실시간 감시와 함께 개발 서버 실행
|
|
33
37
|
build 클라이언트 번들 빌드 (Hydration)
|
|
34
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
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({
|
|
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];
|