@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.
@@ -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
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { specUpsert } from "./commands/spec-upsert";
4
- import { generateApply } from "./commands/generate-apply";
5
- import { guardCheck } from "./commands/guard-check";
6
- import { dev } from "./commands/dev";
3
+ import { specUpsert } from "./commands/spec-upsert";
4
+ import { generateApply } from "./commands/generate-apply";
5
+ import { guardCheck } from "./commands/guard-check";
6
+ import { guardArch } from "./commands/guard-arch";
7
+ import { check } from "./commands/check";
8
+ import { dev } from "./commands/dev";
7
9
  import { init } from "./commands/init";
8
10
  import { build } from "./commands/build";
9
11
  import { contractCreate, contractValidate } from "./commands/contract";
@@ -19,19 +21,30 @@ import {
19
21
  import { doctor } from "./commands/doctor";
20
22
  import { watch } from "./commands/watch";
21
23
  import { brainSetup, brainStatus } from "./commands/brain";
24
+ import { routesGenerate, routesList, routesWatch } from "./commands/routes";
22
25
 
23
26
  const HELP_TEXT = `
24
27
  🥟 Mandu CLI - Agent-Native Fullstack Framework
25
28
 
26
29
  Usage: bunx mandu <command> [options]
27
30
 
28
- Commands:
29
- init 새 프로젝트 생성
30
- spec-upsert Spec 파일 검증 lock 갱신
31
- generate Spec에서 코드 생성
32
- guard Guard 규칙 검사
31
+ Commands:
32
+ init 새 프로젝트 생성
33
+ check FS Routes + Guard 통합 검사
34
+ routes generate FS Routes 스캔 및 매니페스트 생성
35
+ routes list 현재 라우트 목록 출력
36
+ routes watch 실시간 라우트 감시
37
+ dev 개발 서버 실행 (FS Routes + Guard 기본)
38
+ dev --no-guard Guard 감시 비활성화
33
39
  build 클라이언트 번들 빌드 (Hydration)
34
- dev 개발 서버 실행
40
+ guard Guard 규칙 검사 (레거시 Spec 기반)
41
+ guard arch 아키텍처 위반 검사 (FSD/Clean/Hexagonal)
42
+ guard arch --watch 실시간 아키텍처 감시
43
+ guard arch --list-presets 사용 가능한 프리셋 목록
44
+ guard arch --output report.md 리포트 파일 생성
45
+ guard arch --show-trend 트렌드 분석 표시
46
+ spec-upsert Spec 파일 검증 및 lock 갱신 (레거시)
47
+ generate Spec에서 코드 생성 (레거시)
35
48
 
36
49
  doctor Guard 실패 분석 + 패치 제안 (Brain)
37
50
  watch 실시간 파일 감시 - 경고만 (Brain)
@@ -52,47 +65,58 @@ Commands:
52
65
  change list 변경 이력 조회
53
66
  change prune 오래된 스냅샷 정리
54
67
 
55
- Options:
56
- --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
57
- --file <path> spec-upsert 시 사용할 spec 파일 경로
58
- --port <port> dev/openapi serve 포트 (기본: 3000/8080)
59
- --no-auto-correct guard 시 자동 수정 비활성화
68
+ Options:
69
+ --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
70
+ --file <path> spec-upsert 시 사용할 spec 파일 경로
71
+ --port <port> dev/openapi serve 포트 (기본: 3000/8080)
72
+ --guard devArchitecture Guard 실시간 감시 활성화 (기본: ON)
73
+ --no-guard dev 시 Guard 비활성화
74
+ --guard-preset <p> dev --guard 시 프리셋 (기본: mandu)
75
+ --guard-format <f> dev --guard 출력 형식: console, json, agent (기본: 자동)
76
+ --legacy FS Routes 비활성화 (레거시 모드)
77
+ --no-auto-correct guard 시 자동 수정 비활성화
78
+ --preset <name> guard/check 프리셋 (기본: mandu) - fsd, clean, hexagonal, atomic 선택 가능
79
+ --ci guard/check CI 모드 (에러 시 exit 1)
80
+ --quiet guard/check 요약만 출력
81
+ --report-format guard arch 리포트 형식: json, markdown, html
82
+ --save-stats guard arch 통계 저장 (트렌드 분석용)
83
+ --show-trend guard arch 트렌드 분석 표시
60
84
  --minify build 시 코드 압축
61
85
  --sourcemap build 시 소스맵 생성
62
- --watch build 파일 감시 모드
86
+ --watch build/guard arch 파일 감시 모드
63
87
  --message <msg> change begin 시 설명 메시지
64
88
  --id <id> change rollback 시 특정 변경 ID
65
89
  --keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
66
90
  --output <path> openapi/doctor 출력 경로
67
- --format <fmt> doctor 출력 형식: console, json, markdown (기본: console)
91
+ --format <fmt> doctor/guard/check 출력 형식: console, json, agent (기본: 자동)
68
92
  --no-llm doctor에서 LLM 사용 안 함 (템플릿 모드)
69
93
  --model <name> brain setup 시 모델 이름 (기본: llama3.2)
70
94
  --url <url> brain setup 시 Ollama URL
71
95
  --verbose 상세 출력
72
96
  --help, -h 도움말 표시
73
97
 
74
- Examples:
75
- bunx mandu init --name my-app
76
- bunx mandu spec-upsert
77
- bunx mandu generate
98
+ Examples:
99
+ bunx mandu init --name my-app
100
+ bunx mandu check
101
+ bunx mandu routes list
102
+ bunx mandu routes generate
103
+ bunx mandu dev --port 3000
104
+ bunx mandu dev --no-guard
105
+ bunx mandu build --minify
78
106
  bunx mandu guard
79
- bunx mandu build --minify
80
- bunx mandu build --watch
81
- bunx mandu dev --port 3000
107
+ bunx mandu guard arch --preset fsd
108
+ bunx mandu guard arch --watch
109
+ bunx mandu guard arch --ci --format json
82
110
  bunx mandu doctor
83
- bunx mandu doctor --format markdown --output report.md
84
- bunx mandu watch
85
111
  bunx mandu brain setup --model codellama
86
- bunx mandu brain status
87
112
  bunx mandu contract create users
88
- bunx mandu contract validate --verbose
89
113
  bunx mandu openapi generate --output docs/api.json
90
- bunx mandu openapi serve --port 8080
91
114
  bunx mandu change begin --message "Add new route"
92
- bunx mandu change commit
93
- bunx mandu change rollback
94
115
 
95
- Workflow:
116
+ FS Routes Workflow (권장):
117
+ 1. init → 2. app/ 폴더에 page.tsx 생성 → 3. dev → 4. build
118
+
119
+ Legacy Workflow:
96
120
  1. init → 2. spec-upsert → 3. generate → 4. build → 5. guard → 6. dev
97
121
 
98
122
  Contract-first Workflow:
@@ -170,15 +194,46 @@ async function main(): Promise<void> {
170
194
  success = await specUpsert({ file: options.file });
171
195
  break;
172
196
 
173
- case "generate":
174
- success = await generateApply();
175
- break;
176
-
177
- case "guard":
178
- success = await guardCheck({
179
- autoCorrect: options["no-auto-correct"] !== "true",
180
- });
197
+ case "generate":
198
+ success = await generateApply();
199
+ break;
200
+
201
+ case "check":
202
+ success = await check({
203
+ preset: options.preset as any,
204
+ format: options.format as any,
205
+ ci: options.ci === "true",
206
+ quiet: options.quiet === "true",
207
+ legacy: options.legacy === "true",
208
+ });
209
+ break;
210
+
211
+ case "guard": {
212
+ const subCommand = args[1];
213
+ switch (subCommand) {
214
+ case "arch":
215
+ success = await guardArch({
216
+ preset: (options.preset as any) || "fsd",
217
+ watch: options.watch === "true",
218
+ ci: options.ci === "true",
219
+ format: options.format as any,
220
+ quiet: options.quiet === "true",
221
+ srcDir: options["src-dir"],
222
+ listPresets: options["list-presets"] === "true",
223
+ output: options.output,
224
+ reportFormat: (options["report-format"] as any) || "markdown",
225
+ saveStats: options["save-stats"] === "true",
226
+ showTrend: options["show-trend"] === "true",
227
+ });
228
+ break;
229
+ default:
230
+ // 기본값: 레거시 guard-check
231
+ success = await guardCheck({
232
+ autoCorrect: options["no-auto-correct"] !== "true",
233
+ });
234
+ }
181
235
  break;
236
+ }
182
237
 
183
238
  case "build":
184
239
  success = await build({
@@ -188,9 +243,50 @@ async function main(): Promise<void> {
188
243
  });
189
244
  break;
190
245
 
191
- case "dev":
192
- await dev({ port: parsePort(options.port) });
246
+ case "dev":
247
+ await dev({
248
+ port: parsePort(options.port),
249
+ guard: options["no-guard"] === "true" ? false : options.guard !== "false",
250
+ guardPreset: options["guard-preset"] as any,
251
+ guardFormat: options["guard-format"] as any,
252
+ legacy: options.legacy === "true",
253
+ });
254
+ break;
255
+
256
+ case "routes": {
257
+ const subCommand = args[1];
258
+ switch (subCommand) {
259
+ case "generate":
260
+ success = await routesGenerate({
261
+ output: options.output,
262
+ verbose: options.verbose === "true",
263
+ });
264
+ break;
265
+ case "list":
266
+ success = await routesList({
267
+ verbose: options.verbose === "true",
268
+ });
269
+ break;
270
+ case "watch":
271
+ success = await routesWatch({
272
+ output: options.output,
273
+ verbose: options.verbose === "true",
274
+ });
275
+ break;
276
+ default:
277
+ // 기본값: list
278
+ if (!subCommand) {
279
+ success = await routesList({
280
+ verbose: options.verbose === "true",
281
+ });
282
+ } else {
283
+ console.error(`❌ Unknown routes subcommand: ${subCommand}`);
284
+ console.log("\nUsage: bunx mandu routes <generate|list|watch>");
285
+ process.exit(1);
286
+ }
287
+ }
193
288
  break;
289
+ }
194
290
 
195
291
  case "contract": {
196
292
  const subCommand = args[1];
package/src/util/fs.ts CHANGED
@@ -1,9 +1,28 @@
1
- import path from "path";
1
+ import path from "path";
2
+ import fs from "fs/promises";
2
3
 
3
4
  export function resolveFromCwd(...paths: string[]): string {
4
5
  return path.resolve(process.cwd(), ...paths);
5
6
  }
6
7
 
7
- export function getRootDir(): string {
8
- return process.cwd();
9
- }
8
+ export function getRootDir(): string {
9
+ return process.cwd();
10
+ }
11
+
12
+ export async function pathExists(targetPath: string): Promise<boolean> {
13
+ try {
14
+ await fs.access(targetPath);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ export async function isDirectory(targetPath: string): Promise<boolean> {
22
+ try {
23
+ const stat = await fs.stat(targetPath);
24
+ return stat.isDirectory();
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
@@ -0,0 +1,41 @@
1
+ export type OutputFormat = "console" | "agent" | "json";
2
+
3
+ function normalizeFormat(value?: string): OutputFormat | undefined {
4
+ if (!value) return undefined;
5
+ if (value === "console" || value === "agent" || value === "json") {
6
+ return value;
7
+ }
8
+ return undefined;
9
+ }
10
+
11
+ export function resolveOutputFormat(explicit?: OutputFormat): OutputFormat {
12
+ const env = process.env;
13
+
14
+ const direct = normalizeFormat(explicit) ?? normalizeFormat(env.MANDU_OUTPUT);
15
+ if (direct) return direct;
16
+
17
+ const agentSignals = [
18
+ "MANDU_AGENT",
19
+ "CODEX_AGENT",
20
+ "CODEX",
21
+ "CLAUDE_CODE",
22
+ "ANTHROPIC_CLAUDE_CODE",
23
+ ];
24
+
25
+ for (const key of agentSignals) {
26
+ const value = env[key];
27
+ if (value === "1" || value === "true") {
28
+ return "json";
29
+ }
30
+ }
31
+
32
+ if (env.CI === "true") {
33
+ return "json";
34
+ }
35
+
36
+ if (process.stdout && !process.stdout.isTTY) {
37
+ return "json";
38
+ }
39
+
40
+ return "console";
41
+ }