@mandujs/cli 0.9.17 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center">
8
8
  <strong>Agent-Native Fullstack Framework CLI</strong><br/>
9
- A development OS where architecture stays intact even when AI agents write your code
9
+ Architecture stays intact even when AI agents write your code
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -16,47 +16,26 @@
16
16
  ## Installation
17
17
 
18
18
  ```bash
19
- # Bun required
20
19
  bun add -D @mandujs/cli
21
20
  ```
22
21
 
23
- ## Quick Start
22
+ Or use directly with `bunx`:
24
23
 
25
24
  ```bash
26
- # Create a new project
27
25
  bunx @mandujs/cli init my-app
28
- cd my-app
29
-
30
- # Start development server
31
- bun run dev
32
26
  ```
33
27
 
34
- ## Commands
35
-
36
- ### `mandu init <project-name>`
28
+ ## Quick Start
37
29
 
38
- Creates a new Mandu project.
30
+ ### 1. Create a New Project
39
31
 
40
32
  ```bash
41
33
  bunx @mandujs/cli init my-app
34
+ cd my-app
35
+ bun install
42
36
  ```
43
37
 
44
- Generated structure:
45
- ```
46
- my-app/
47
- ├── apps/
48
- │ ├── server/main.ts # Server entry point
49
- │ └── web/entry.tsx # Client entry point
50
- ├── spec/
51
- │ └── routes.manifest.json # SSOT - Route definitions
52
- ├── tests/ # Test templates
53
- ├── package.json
54
- └── tsconfig.json
55
- ```
56
-
57
- ### `mandu dev`
58
-
59
- Starts the development server (with HMR support).
38
+ ### 2. Start Development Server
60
39
 
61
40
  ```bash
62
41
  bun run dev
@@ -64,117 +43,264 @@ bun run dev
64
43
  bunx mandu dev
65
44
  ```
66
45
 
67
- ### `mandu spec`
46
+ Your app is now running at `http://localhost:3000`.
47
+
48
+ ### 3. Create Pages in `app/` Directory
49
+
50
+ ```
51
+ app/
52
+ ├── page.tsx → /
53
+ ├── about/page.tsx → /about
54
+ ├── users/[id]/page.tsx → /users/:id
55
+ └── api/hello/route.ts → /api/hello
56
+ ```
68
57
 
69
- Validates the spec file and updates the lock file.
58
+ ### 4. Build for Production
70
59
 
71
60
  ```bash
72
- bun run spec
61
+ bunx mandu build
73
62
  ```
74
63
 
75
- ### `mandu generate`
64
+ That's it!
65
+
66
+ ---
67
+
68
+ ## Commands
69
+
70
+ ### Core Commands
71
+
72
+ | Command | Description |
73
+ |---------|-------------|
74
+ | `mandu init [name]` | Create new project |
75
+ | `mandu dev` | Start dev server (FS Routes + HMR) |
76
+ | `mandu build` | Build for production |
77
+
78
+ ### FS Routes Commands
79
+
80
+ | Command | Description |
81
+ |---------|-------------|
82
+ | `mandu routes list` | Show all routes |
83
+ | `mandu routes generate` | Generate routes manifest |
84
+ | `mandu routes watch` | Watch for route changes |
85
+
86
+ ### Guard Commands
87
+
88
+ | Command | Description |
89
+ |---------|-------------|
90
+ | `mandu guard arch` | Run architecture check (default: mandu preset) |
91
+ | `mandu guard arch --watch` | Watch mode |
92
+ | `mandu guard arch --ci` | CI mode (exit 1 on errors) |
93
+ | `mandu guard arch --preset fsd` | Use specific preset |
94
+ | `mandu guard arch --output report.md` | Generate report |
95
+
96
+ ### Transaction Commands
97
+
98
+ | Command | Description |
99
+ |---------|-------------|
100
+ | `mandu change begin` | Start transaction (creates snapshot) |
101
+ | `mandu change commit` | Finalize changes |
102
+ | `mandu change rollback` | Restore from snapshot |
103
+ | `mandu change status` | Show current state |
104
+ | `mandu change list` | View history |
105
+
106
+ ### Brain Commands
107
+
108
+ | Command | Description |
109
+ |---------|-------------|
110
+ | `mandu doctor` | Analyze Guard failures + suggest patches |
111
+ | `mandu watch` | Real-time file monitoring |
112
+ | `mandu brain setup` | Configure sLLM (optional) |
113
+ | `mandu brain status` | Check Brain status |
114
+
115
+ ### Contract & OpenAPI Commands
116
+
117
+ | Command | Description |
118
+ |---------|-------------|
119
+ | `mandu contract create <routeId>` | Create contract for route |
120
+ | `mandu contract validate` | Validate contract-slot consistency |
121
+ | `mandu openapi generate` | Generate OpenAPI 3.0 spec |
122
+ | `mandu openapi serve` | Start Swagger UI server |
123
+
124
+ ---
76
125
 
77
- Generates code based on the spec.
126
+ ## Workflow
127
+
128
+ ### Modern Workflow (Recommended)
78
129
 
79
130
  ```bash
80
- bun run generate
81
- ```
131
+ # 1. Create project
132
+ bunx @mandujs/cli init my-app
133
+ cd my-app && bun install
134
+
135
+ # 2. Create pages
136
+ # app/page.tsx → /
137
+ # app/users/page.tsx → /users
138
+ # app/api/users/route.ts → /api/users
82
139
 
83
- ### `mandu guard`
140
+ # 3. Start development
141
+ bun run dev
142
+ ```
84
143
 
85
- Checks architecture rules and auto-corrects violations.
144
+ ### With Architecture Guard
86
145
 
87
146
  ```bash
88
- bun run guard
147
+ # Development with Guard watching
148
+ bunx mandu dev --guard
89
149
 
90
- # Disable auto-correction
91
- bunx mandu guard --no-auto-correct
150
+ # Or run Guard separately
151
+ bunx mandu guard arch --watch
92
152
  ```
93
153
 
94
- Auto-correctable rules:
95
- - `SPEC_HASH_MISMATCH` → Updates lock file
96
- - `GENERATED_MANUAL_EDIT` → Regenerates code
97
- - `SLOT_NOT_FOUND` Creates slot file
98
-
99
- ## Writing Spec Files
100
-
101
- `spec/routes.manifest.json` is the Single Source of Truth (SSOT) for all routes.
102
-
103
- ```json
104
- {
105
- "version": "1.0.0",
106
- "routes": [
107
- {
108
- "id": "getUsers",
109
- "pattern": "/api/users",
110
- "kind": "api",
111
- "module": "apps/server/api/users.ts"
112
- },
113
- {
114
- "id": "homePage",
115
- "pattern": "/",
116
- "kind": "page",
117
- "module": "apps/server/pages/home.ts",
118
- "componentModule": "apps/web/pages/Home.tsx"
119
- }
120
- ]
121
- }
154
+ ### CI/CD Integration
155
+
156
+ ```bash
157
+ # Build and check
158
+ bunx mandu build --minify
159
+ bunx mandu guard arch --ci --format json
122
160
  ```
123
161
 
124
- ### Slot System (v0.2.0+)
162
+ ---
125
163
 
126
- Add `slotModule` to separate business logic:
164
+ ## FS Routes
127
165
 
128
- ```json
129
- {
130
- "id": "getUsers",
131
- "pattern": "/api/users",
132
- "kind": "api",
133
- "module": "apps/server/api/users.generated.ts",
134
- "slotModule": "apps/server/api/users.slot.ts"
135
- }
166
+ Create routes by adding files to `app/`:
167
+
168
+ ```
169
+ app/
170
+ ├── page.tsx → /
171
+ ├── layout.tsx → Layout for all pages
172
+ ├── users/
173
+ │ ├── page.tsx → /users
174
+ │ ├── [id]/
175
+ │ │ └── page.tsx → /users/:id
176
+ │ └── [...slug]/
177
+ │ └── page.tsx → /users/*
178
+ ├── api/
179
+ │ └── users/
180
+ │ └── route.ts → /api/users
181
+ └── (auth)/ → Group (no URL segment)
182
+ └── login/page.tsx → /login
136
183
  ```
137
184
 
138
- - `*.generated.ts` - Managed by framework (do not modify)
139
- - `*.slot.ts` - Business logic written by developers
185
+ ### Special Files
140
186
 
141
- ## Development Workflow
187
+ | File | Purpose |
188
+ |------|---------|
189
+ | `page.tsx` | Page component |
190
+ | `layout.tsx` | Shared layout |
191
+ | `route.ts` | API endpoint |
192
+ | `loading.tsx` | Loading state |
193
+ | `error.tsx` | Error boundary |
194
+ | `slot.ts` | Business logic |
195
+ | `client.tsx` | Interactive component (Island) |
142
196
 
143
- ```bash
144
- # 1. Edit spec
145
- # 2. Validate spec and update lock
146
- bun run spec
197
+ ---
147
198
 
148
- # 3. Generate code
149
- bun run generate
199
+ ## Guard Presets
150
200
 
151
- # 4. Check architecture
152
- bun run guard
201
+ | Preset | Description |
202
+ |--------|-------------|
203
+ | `mandu` | FSD + Clean Architecture (default) |
204
+ | `fsd` | Feature-Sliced Design |
205
+ | `clean` | Clean Architecture |
206
+ | `hexagonal` | Hexagonal Architecture |
207
+ | `atomic` | Atomic Design |
153
208
 
154
- # 5. Run tests
155
- bun test
209
+ ```bash
210
+ # List all presets
211
+ bunx mandu guard arch --list-presets
156
212
 
157
- # 6. Start dev server
158
- bun run dev
213
+ # Use specific preset
214
+ bunx mandu guard arch --preset fsd
159
215
  ```
160
216
 
161
- ## Testing
217
+ ---
218
+
219
+ ## Options Reference
220
+
221
+ ### `mandu dev`
222
+
223
+ | Option | Description |
224
+ |--------|-------------|
225
+ | `--port <n>` | Server port (default: 3000) |
226
+ | `--guard` | Enable Guard watching |
227
+ | `--guard-preset <p>` | Guard preset (default: mandu) |
228
+
229
+ ### `mandu build`
230
+
231
+ | Option | Description |
232
+ |--------|-------------|
233
+ | `--minify` | Minify output |
234
+ | `--sourcemap` | Generate sourcemaps |
235
+ | `--watch` | Watch mode |
236
+
237
+ ### `mandu guard arch`
238
+
239
+ | Option | Description |
240
+ |--------|-------------|
241
+ | `--preset <p>` | Preset: fsd, clean, hexagonal, atomic, mandu |
242
+ | `--watch` | Watch mode |
243
+ | `--ci` | CI mode (exit 1 on errors) |
244
+ | `--quiet` | Summary only |
245
+ | `--format <f>` | Output: console, agent, json |
246
+ | `--output <path>` | Report file path |
247
+ | `--report-format <f>` | Report: json, markdown, html |
248
+ | `--save-stats` | Save for trend analysis |
249
+ | `--show-trend` | Show trend analysis |
250
+
251
+ ### `mandu doctor`
252
+
253
+ | Option | Description |
254
+ |--------|-------------|
255
+ | `--format <f>` | Output: console, json, markdown |
256
+ | `--no-llm` | Template mode (no LLM) |
257
+ | `--output <path>` | Output file path |
258
+
259
+ ---
162
260
 
163
- Built-in support for Bun test framework.
261
+ ## Examples
164
262
 
165
263
  ```bash
166
- bun test # Run tests
167
- bun test --watch # Watch mode
264
+ # Initialize project
265
+ bunx @mandujs/cli init my-app
266
+
267
+ # Development
268
+ bunx mandu dev --port 3000
269
+ bunx mandu dev --guard
270
+
271
+ # Routes
272
+ bunx mandu routes list
273
+ bunx mandu routes generate
274
+
275
+ # Guard
276
+ bunx mandu guard arch
277
+ bunx mandu guard arch --watch
278
+ bunx mandu guard arch --ci --format json
279
+ bunx mandu guard arch --output report.md
280
+
281
+ # Transactions
282
+ bunx mandu change begin --message "Add users API"
283
+ bunx mandu change commit
284
+ bunx mandu change rollback
285
+
286
+ # Doctor
287
+ bunx mandu doctor
288
+ bunx mandu doctor --format json
289
+
290
+ # Build
291
+ bunx mandu build --minify --sourcemap
168
292
  ```
169
293
 
294
+ ---
295
+
170
296
  ## Requirements
171
297
 
172
298
  - Bun >= 1.0.0
173
- - React >= 18.0.0
174
299
 
175
300
  ## Related Packages
176
301
 
177
302
  - [@mandujs/core](https://www.npmjs.com/package/@mandujs/core) - Core runtime
303
+ - [@mandujs/mcp](https://www.npmjs.com/package/@mandujs/mcp) - MCP server
178
304
 
179
305
  ## License
180
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
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.37"
35
+ "@mandujs/core": "0.9.38"
36
36
  },
37
37
  "engines": {
38
38
  "bun": ">=1.0.0"
@@ -0,0 +1,204 @@
1
+ /**
2
+ * mandu check - Workflow Check Command
3
+ *
4
+ * FS Routes + Architecture Guard + Legacy Guard 통합 검사
5
+ */
6
+
7
+ import {
8
+ generateManifest,
9
+ scanRoutes,
10
+ checkDirectory,
11
+ printReport,
12
+ getPreset,
13
+ loadManifest,
14
+ runGuardCheck,
15
+ buildGuardReport,
16
+ printReportSummary,
17
+ type GuardConfig,
18
+ type GuardPreset,
19
+ } from "@mandujs/core";
20
+ import path from "path";
21
+ import { resolveFromCwd, isDirectory, pathExists } from "../util/fs";
22
+ import { resolveOutputFormat, type OutputFormat } from "../util/output";
23
+
24
+ export interface CheckOptions {
25
+ preset?: GuardPreset;
26
+ format?: OutputFormat;
27
+ ci?: boolean;
28
+ quiet?: boolean;
29
+ legacy?: boolean;
30
+ }
31
+
32
+ export async function check(options: CheckOptions = {}): Promise<boolean> {
33
+ const rootDir = resolveFromCwd(".");
34
+ const preset = options.preset ?? "mandu";
35
+ const format = resolveOutputFormat(options.format);
36
+ const quiet = options.quiet === true;
37
+ const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
38
+ const specPath = resolveFromCwd("spec/routes.manifest.json");
39
+ const hasSpec = await pathExists(specPath);
40
+
41
+ let success = true;
42
+
43
+ const log = (message: string) => {
44
+ if (format === "console" && !quiet) {
45
+ console.log(message);
46
+ }
47
+ };
48
+
49
+ const print = (message: string) => {
50
+ if (format === "console") {
51
+ console.log(message);
52
+ }
53
+ };
54
+
55
+ if (format === "console") {
56
+ log("🥟 Mandu Check\n");
57
+ }
58
+
59
+ // 1) FS Routes 검사
60
+ let routesSummary: { enabled: boolean; count: number; warnings: string[] } = {
61
+ enabled: false,
62
+ count: 0,
63
+ warnings: [],
64
+ };
65
+
66
+ if (enableFsRoutes) {
67
+ routesSummary.enabled = true;
68
+
69
+ try {
70
+ if (format === "console") {
71
+ const result = await generateManifest(rootDir, {
72
+ outputPath: ".mandu/routes.manifest.json",
73
+ skipLegacy: true,
74
+ });
75
+ routesSummary.count = result.manifest.routes.length;
76
+ routesSummary.warnings = result.warnings;
77
+
78
+ if (quiet) {
79
+ print(`✅ FS Routes: ${routesSummary.count}개`);
80
+ } else {
81
+ log(`✅ FS Routes: ${routesSummary.count}개`);
82
+ }
83
+ if (routesSummary.warnings.length > 0) {
84
+ if (!quiet) {
85
+ log("⚠️ 경고:");
86
+ }
87
+ for (const warning of routesSummary.warnings) {
88
+ if (!quiet) {
89
+ log(` - ${warning}`);
90
+ }
91
+ }
92
+ }
93
+ if (!quiet) {
94
+ log("");
95
+ }
96
+ } else {
97
+ const scan = await scanRoutes(rootDir);
98
+ routesSummary.count = scan.routes.length;
99
+ routesSummary.warnings = scan.errors.map((e) => `${e.type}: ${e.message}`);
100
+ }
101
+ } catch (error) {
102
+ success = false;
103
+ routesSummary.warnings.push(
104
+ error instanceof Error ? error.message : String(error)
105
+ );
106
+ if (format === "console") {
107
+ console.error("❌ FS Routes 검사 실패:", error);
108
+ }
109
+ }
110
+ } else {
111
+ if (quiet) {
112
+ print("ℹ️ app/ 폴더 없음 - FS Routes 검사 스킵");
113
+ } else {
114
+ log("ℹ️ app/ 폴더 없음 - FS Routes 검사 스킵\n");
115
+ }
116
+ }
117
+
118
+ // 2) Architecture Guard 검사
119
+ const guardConfig: GuardConfig = {
120
+ preset,
121
+ srcDir: "src",
122
+ fsRoutes: enableFsRoutes
123
+ ? {
124
+ noPageToPage: true,
125
+ pageCanImport: ["widgets", "features", "entities", "shared"],
126
+ layoutCanImport: ["widgets", "shared"],
127
+ }
128
+ : undefined,
129
+ };
130
+
131
+ const report = await checkDirectory(guardConfig, rootDir);
132
+ if (report.bySeverity.error > 0) {
133
+ success = false;
134
+ }
135
+
136
+ if (format === "console") {
137
+ const presetDef = getPreset(preset);
138
+ if (quiet) {
139
+ print(`📊 Architecture: ${report.totalViolations}개 위반 (Errors: ${report.bySeverity.error})`);
140
+ } else {
141
+ printReport(report, presetDef.hierarchy);
142
+ }
143
+ }
144
+
145
+ // 3) Legacy Guard 검사 (spec 파일이 있을 때만)
146
+ let legacySummary: { enabled: boolean; passed: boolean; violations: number; errors?: string[] } = {
147
+ enabled: false,
148
+ passed: true,
149
+ violations: 0,
150
+ };
151
+
152
+ if (hasSpec) {
153
+ legacySummary.enabled = true;
154
+
155
+ const manifestResult = await loadManifest(specPath);
156
+ if (!manifestResult.success || !manifestResult.data) {
157
+ legacySummary.passed = false;
158
+ legacySummary.errors = manifestResult.errors ?? ["Spec 로드 실패"];
159
+ success = false;
160
+
161
+ if (format === "console") {
162
+ console.error("❌ Spec 로드 실패:");
163
+ manifestResult.errors?.forEach((e) => console.error(` - ${e}`));
164
+ }
165
+ } else {
166
+ const checkResult = await runGuardCheck(manifestResult.data, rootDir);
167
+ legacySummary.passed = checkResult.passed;
168
+ legacySummary.violations = checkResult.violations.length;
169
+ success = success && checkResult.passed;
170
+
171
+ if (format === "console") {
172
+ const legacyReport = buildGuardReport(checkResult);
173
+ if (quiet) {
174
+ print(`📊 Legacy Guard: ${legacySummary.violations}개 위반`);
175
+ } else {
176
+ printReportSummary(legacyReport);
177
+ }
178
+ }
179
+ }
180
+ } else {
181
+ if (quiet) {
182
+ print("ℹ️ spec/routes.manifest.json 없음 - 레거시 Guard 스킵");
183
+ } else {
184
+ log("ℹ️ spec/routes.manifest.json 없음 - 레거시 Guard 스킵");
185
+ }
186
+ }
187
+
188
+ if (format !== "console") {
189
+ const summary = {
190
+ ok: success,
191
+ routes: routesSummary,
192
+ architecture: {
193
+ totalViolations: report.totalViolations,
194
+ bySeverity: report.bySeverity,
195
+ byType: report.byType,
196
+ report,
197
+ },
198
+ legacy: legacySummary,
199
+ };
200
+ console.log(JSON.stringify(summary, null, 2));
201
+ }
202
+
203
+ return options.ci ? success : success;
204
+ }
@@ -17,20 +17,23 @@ import {
17
17
  type GuardConfig,
18
18
  type GuardPreset,
19
19
  } from "@mandujs/core";
20
- import { resolveFromCwd } from "../util/fs";
20
+ import { isDirectory, resolveFromCwd } from "../util/fs";
21
+ import { resolveOutputFormat, type OutputFormat } from "../util/output";
21
22
  import path from "path";
22
23
 
23
- export interface DevOptions {
24
- port?: number;
25
- /** HMR 비활성화 */
26
- noHmr?: boolean;
27
- /** FS Routes 비활성화 (레거시 모드) */
28
- legacy?: boolean;
29
- /** Architecture Guard 활성화 */
30
- guard?: boolean;
31
- /** Guard 프리셋 */
32
- guardPreset?: GuardPreset;
33
- }
24
+ export interface DevOptions {
25
+ port?: number;
26
+ /** HMR 비활성화 */
27
+ noHmr?: boolean;
28
+ /** FS Routes 비활성화 (레거시 모드) */
29
+ legacy?: boolean;
30
+ /** Architecture Guard 활성화 */
31
+ guard?: boolean;
32
+ /** Guard 프리셋 */
33
+ guardPreset?: GuardPreset;
34
+ /** Guard 출력 형식 */
35
+ guardFormat?: OutputFormat;
36
+ }
34
37
 
35
38
  export async function dev(options: DevOptions = {}): Promise<void> {
36
39
  const rootDir = resolveFromCwd(".");
@@ -203,16 +206,26 @@ export async function dev(options: DevOptions = {}): Promise<void> {
203
206
  bundleManifest: devBundler?.initialBuild.manifest,
204
207
  });
205
208
 
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
- };
209
+ // Architecture Guard 실시간 감시 (선택적)
210
+ let archGuardWatcher: ReturnType<typeof createGuardWatcher> | null = null;
211
+
212
+ if (options.guard !== false) {
213
+ const guardPreset = options.guardPreset || "mandu";
214
+ const guardFormat = resolveOutputFormat(options.guardFormat);
215
+ const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
216
+ const guardConfig: GuardConfig = {
217
+ preset: guardPreset,
218
+ srcDir: "src",
219
+ realtime: true,
220
+ realtimeOutput: guardFormat,
221
+ fsRoutes: enableFsRoutes
222
+ ? {
223
+ noPageToPage: true,
224
+ pageCanImport: ["widgets", "features", "entities", "shared"],
225
+ layoutCanImport: ["widgets", "shared"],
226
+ }
227
+ : undefined,
228
+ };
216
229
 
217
230
  console.log(`🛡️ Architecture Guard 활성화 (${guardPreset})`);
218
231
 
@@ -10,7 +10,7 @@ import {
10
10
  runGuardCheck,
11
11
  analyzeViolations,
12
12
  printDoctorReport,
13
- generateMarkdownReport,
13
+ generateDoctorMarkdownReport,
14
14
  initializeBrain,
15
15
  getBrain,
16
16
  } from "../../../core/src/index";
@@ -108,7 +108,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
108
108
  }
109
109
 
110
110
  case "markdown": {
111
- const md = generateMarkdownReport(analysis);
111
+ const md = generateDoctorMarkdownReport(analysis);
112
112
 
113
113
  if (output) {
114
114
  await fs.writeFile(output, md, "utf-8");
@@ -17,13 +17,15 @@ import {
17
17
  loadStatistics,
18
18
  analyzeTrend,
19
19
  calculateLayerStatistics,
20
- generateMarkdownReport,
20
+ generateGuardMarkdownReport,
21
21
  generateHTMLReport,
22
22
  type GuardConfig,
23
23
  type GuardPreset,
24
24
  } from "@mandujs/core";
25
25
  import { writeFile } from "fs/promises";
26
- import { resolveFromCwd } from "../util/fs";
26
+ import { isDirectory, resolveFromCwd } from "../util/fs";
27
+ import { resolveOutputFormat, type OutputFormat } from "../util/output";
28
+ import path from "path";
27
29
 
28
30
  export interface GuardArchOptions {
29
31
  /** 프리셋 이름 */
@@ -33,7 +35,7 @@ export interface GuardArchOptions {
33
35
  /** CI 모드 (에러 시 exit 1) */
34
36
  ci?: boolean;
35
37
  /** 출력 형식: console, agent, json */
36
- format?: "console" | "agent" | "json";
38
+ format?: OutputFormat;
37
39
  /** 조용히 (요약만 출력) */
38
40
  quiet?: boolean;
39
41
  /** 소스 디렉토리 */
@@ -55,7 +57,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
55
57
  preset = "mandu",
56
58
  watch = false,
57
59
  ci = false,
58
- format = "console",
60
+ format,
59
61
  quiet = false,
60
62
  srcDir = "src",
61
63
  listPresets: showPresets = false,
@@ -66,6 +68,8 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
66
68
  } = options;
67
69
 
68
70
  const rootDir = resolveFromCwd(".");
71
+ const resolvedFormat = resolveOutputFormat(format);
72
+ const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
69
73
 
70
74
  // 프리셋 목록 출력
71
75
  if (showPresets) {
@@ -86,25 +90,37 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
86
90
  return true;
87
91
  }
88
92
 
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("");
93
+ if (resolvedFormat === "console") {
94
+ console.log("");
95
+ console.log("🛡️ Mandu Guard - Architecture Checker");
96
+ console.log("");
97
+ console.log(`📋 Preset: ${preset}`);
98
+ console.log(`📂 Source: ${srcDir}/`);
99
+ console.log(`🔧 Mode: ${watch ? "Watch" : "Check"}`);
100
+ console.log("");
101
+ }
96
102
 
97
103
  // Guard 설정
98
104
  const config: GuardConfig = {
99
105
  preset,
100
106
  srcDir,
101
107
  realtime: watch,
108
+ realtimeOutput: resolvedFormat,
109
+ fsRoutes: enableFsRoutes
110
+ ? {
111
+ noPageToPage: true,
112
+ pageCanImport: ["widgets", "features", "entities", "shared"],
113
+ layoutCanImport: ["widgets", "shared"],
114
+ }
115
+ : undefined,
102
116
  };
103
117
 
104
118
  // 실시간 감시 모드
105
119
  if (watch) {
106
- console.log("👁️ Watching for architecture violations...");
107
- console.log(" Press Ctrl+C to stop\n");
120
+ if (resolvedFormat === "console") {
121
+ console.log("👁️ Watching for architecture violations...");
122
+ console.log(" Press Ctrl+C to stop\n");
123
+ }
108
124
 
109
125
  const watcher = createGuardWatcher({
110
126
  config,
@@ -113,7 +129,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
113
129
  // 실시간 위반 출력은 watcher 내부에서 처리됨
114
130
  },
115
131
  onFileAnalyzed: (analysis, violations) => {
116
- if (violations.length > 0 && !quiet) {
132
+ if (resolvedFormat === "console" && violations.length > 0 && !quiet) {
117
133
  const timestamp = new Date().toLocaleTimeString();
118
134
  console.log(`[${timestamp}] ${analysis.filePath}: ${violations.length} violation(s)`);
119
135
  }
@@ -134,13 +150,15 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
134
150
  }
135
151
 
136
152
  // 일회성 검사 모드
137
- console.log("🔍 Scanning for architecture violations...\n");
153
+ if (resolvedFormat === "console" && !quiet) {
154
+ console.log("🔍 Scanning for architecture violations...\n");
155
+ }
138
156
 
139
157
  const report = await checkDirectory(config, rootDir);
140
158
  const presetDef = getPreset(preset);
141
159
 
142
160
  // 출력 형식에 따른 리포트 출력
143
- switch (format) {
161
+ switch (resolvedFormat) {
144
162
  case "json":
145
163
  console.log(formatReportAsAgentJSON(report, preset));
146
164
  break;
@@ -209,7 +227,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
209
227
  break;
210
228
  case "markdown":
211
229
  default:
212
- reportContent = generateMarkdownReport(report, trend, layerStats ?? undefined);
230
+ reportContent = generateGuardMarkdownReport(report, trend, layerStats ?? undefined);
213
231
  break;
214
232
  }
215
233
 
package/src/main.ts CHANGED
@@ -1,10 +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 { guardArch } from "./commands/guard-arch";
7
- 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";
8
9
  import { init } from "./commands/init";
9
10
  import { build } from "./commands/build";
10
11
  import { contractCreate, contractValidate } from "./commands/contract";
@@ -27,13 +28,14 @@ const HELP_TEXT = `
27
28
 
28
29
  Usage: bunx mandu <command> [options]
29
30
 
30
- Commands:
31
- init 새 프로젝트 생성
32
- routes generate FS Routes 스캔 매니페스트 생성
33
- routes list 현재 라우트 목록 출력
34
- routes watch 실시간 라우트 감시
35
- dev 개발 서버 실행 (FS Routes 자동 적용)
36
- dev --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 감시 비활성화
37
39
  build 클라이언트 번들 빌드 (Hydration)
38
40
  guard Guard 규칙 검사 (레거시 Spec 기반)
39
41
  guard arch 아키텍처 위반 검사 (FSD/Clean/Hexagonal)
@@ -63,16 +65,19 @@ Commands:
63
65
  change list 변경 이력 조회
64
66
  change prune 오래된 스냅샷 정리
65
67
 
66
- Options:
67
- --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
68
- --file <path> spec-upsert 시 사용할 spec 파일 경로
69
- --port <port> dev/openapi serve 포트 (기본: 3000/8080)
70
- --guard dev 시 Architecture Guard 실시간 감시 활성화
71
- --guard-preset <p> dev --guard 프리셋 (기본: mandu)
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 요약만 출력
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 dev 시 Architecture 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 요약만 출력
76
81
  --report-format guard arch 리포트 형식: json, markdown, html
77
82
  --save-stats guard arch 통계 저장 (트렌드 분석용)
78
83
  --show-trend guard arch 트렌드 분석 표시
@@ -83,19 +88,21 @@ Options:
83
88
  --id <id> change rollback 시 특정 변경 ID
84
89
  --keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
85
90
  --output <path> openapi/doctor 출력 경로
86
- --format <fmt> doctor/guard 출력 형식: console, json, agent
91
+ --format <fmt> doctor/guard/check 출력 형식: console, json, agent (기본: 자동)
87
92
  --no-llm doctor에서 LLM 사용 안 함 (템플릿 모드)
88
93
  --model <name> brain setup 시 모델 이름 (기본: llama3.2)
89
94
  --url <url> brain setup 시 Ollama URL
90
95
  --verbose 상세 출력
91
96
  --help, -h 도움말 표시
92
97
 
93
- Examples:
94
- bunx mandu init --name my-app
95
- bunx mandu routes list
96
- bunx mandu routes generate
97
- bunx mandu dev --port 3000
98
- bunx mandu build --minify
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
99
106
  bunx mandu guard
100
107
  bunx mandu guard arch --preset fsd
101
108
  bunx mandu guard arch --watch
@@ -187,23 +194,33 @@ async function main(): Promise<void> {
187
194
  success = await specUpsert({ file: options.file });
188
195
  break;
189
196
 
190
- case "generate":
191
- success = await generateApply();
192
- break;
193
-
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,
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,
207
224
  reportFormat: (options["report-format"] as any) || "markdown",
208
225
  saveStats: options["save-stats"] === "true",
209
226
  showTrend: options["show-trend"] === "true",
@@ -226,13 +243,15 @@ async function main(): Promise<void> {
226
243
  });
227
244
  break;
228
245
 
229
- case "dev":
230
- await dev({
231
- port: parsePort(options.port),
232
- guard: options.guard === "true",
233
- guardPreset: options["guard-preset"] as any,
234
- });
235
- break;
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;
236
255
 
237
256
  case "routes": {
238
257
  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
+ }