@mandujs/core 0.9.37 → 0.9.38
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 +382 -95
- package/package.json +1 -1
- package/src/brain/doctor/index.ts +1 -1
- package/src/brain/doctor/reporter.ts +3 -3
- package/src/guard/index.ts +1 -1
- package/src/guard/statistics.ts +3 -3
- package/src/guard/types.ts +4 -0
- package/src/guard/watcher.ts +20 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Mandu Framework Core</strong><br/>
|
|
9
|
-
|
|
9
|
+
Runtime, FS Routes, Guard, Bundler, Contract, Filling
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -21,170 +21,456 @@ bun add @mandujs/core
|
|
|
21
21
|
|
|
22
22
|
> Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Basic API Handler (Filling)
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Mandu } from "@mandujs/core";
|
|
30
|
+
|
|
31
|
+
export default Mandu.filling()
|
|
32
|
+
.guard((ctx) => {
|
|
33
|
+
if (!ctx.get("user")) return ctx.unauthorized("Login required");
|
|
34
|
+
})
|
|
35
|
+
.get(async (ctx) => {
|
|
36
|
+
const users = await db.users.findMany();
|
|
37
|
+
return ctx.ok({ data: users });
|
|
38
|
+
})
|
|
39
|
+
.post(async (ctx) => {
|
|
40
|
+
const body = await ctx.body<{ name: string }>();
|
|
41
|
+
const user = await db.users.create({ data: body });
|
|
42
|
+
return ctx.created({ data: user });
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Type-Safe Contract
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { Mandu } from "@mandujs/core";
|
|
50
|
+
import { z } from "zod";
|
|
51
|
+
|
|
52
|
+
const userContract = Mandu.contract({
|
|
53
|
+
request: {
|
|
54
|
+
GET: { query: z.object({ id: z.string() }) },
|
|
55
|
+
POST: { body: z.object({ name: z.string() }) }
|
|
56
|
+
},
|
|
57
|
+
response: {
|
|
58
|
+
200: z.object({ data: z.any() }),
|
|
59
|
+
400: z.object({ error: z.string() })
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const handlers = Mandu.handler(userContract, {
|
|
64
|
+
GET: (ctx) => ({ data: fetchUser(ctx.query.id) }),
|
|
65
|
+
POST: (ctx) => ({ data: createUser(ctx.body) })
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Module Overview
|
|
25
72
|
|
|
26
73
|
```
|
|
27
74
|
@mandujs/core
|
|
28
|
-
├──
|
|
29
|
-
├──
|
|
30
|
-
├──
|
|
31
|
-
├──
|
|
32
|
-
|
|
75
|
+
├── router/ # FS Routes - file-system based routing
|
|
76
|
+
├── guard/ # Mandu Guard - architecture enforcement
|
|
77
|
+
├── runtime/ # Server, SSR, streaming
|
|
78
|
+
├── filling/ # Handler chain API (Mandu.filling())
|
|
79
|
+
├── contract/ # Type-safe API contracts
|
|
80
|
+
├── bundler/ # Client bundling, HMR
|
|
81
|
+
├── client/ # Island hydration, client router
|
|
82
|
+
├── brain/ # Doctor, Watcher, Architecture analyzer
|
|
83
|
+
├── change/ # Transaction & history
|
|
84
|
+
└── spec/ # Manifest schema & validation
|
|
33
85
|
```
|
|
34
86
|
|
|
35
|
-
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## FS Routes
|
|
36
90
|
|
|
37
|
-
|
|
91
|
+
File-system based routing system.
|
|
38
92
|
|
|
39
93
|
```typescript
|
|
40
|
-
import {
|
|
94
|
+
import { scanRoutes, generateManifest, watchFSRoutes } from "@mandujs/core/router";
|
|
41
95
|
|
|
42
|
-
//
|
|
43
|
-
const result = await
|
|
96
|
+
// Scan routes from app/ directory
|
|
97
|
+
const result = await scanRoutes("/path/to/project");
|
|
98
|
+
console.log(result.routes);
|
|
44
99
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
manifest.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
100
|
+
// Generate manifest
|
|
101
|
+
const { manifest } = await generateManifest("/path/to/project", {
|
|
102
|
+
outputPath: ".mandu/manifest.json"
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Watch for changes
|
|
106
|
+
const watcher = await watchFSRoutes("/path/to/project", {
|
|
107
|
+
onChange: (result) => console.log("Routes updated!", result.routes.length)
|
|
108
|
+
});
|
|
51
109
|
```
|
|
52
110
|
|
|
53
|
-
###
|
|
111
|
+
### Route Patterns
|
|
54
112
|
|
|
55
113
|
```typescript
|
|
56
|
-
import {
|
|
114
|
+
import { pathToPattern, parseSegments } from "@mandujs/core/router";
|
|
115
|
+
|
|
116
|
+
// Convert path to URL pattern
|
|
117
|
+
pathToPattern("users/[id]/posts"); // → "/users/:id/posts"
|
|
118
|
+
pathToPattern("docs/[...slug]"); // → "/docs/:slug*"
|
|
119
|
+
pathToPattern("(auth)/login"); // → "/login" (group ignored)
|
|
120
|
+
|
|
121
|
+
// Parse segments
|
|
122
|
+
parseSegments("[id]"); // → [{ type: "dynamic", name: "id" }]
|
|
123
|
+
parseSegments("[...slug]"); // → [{ type: "catch-all", name: "slug" }]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
57
127
|
|
|
58
|
-
|
|
59
|
-
const lock = await writeLock("spec/spec.lock.json", manifest);
|
|
60
|
-
console.log(lock.routesHash);
|
|
128
|
+
## Mandu Guard
|
|
61
129
|
|
|
62
|
-
|
|
63
|
-
|
|
130
|
+
Real-time architecture enforcement with preset support.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import {
|
|
134
|
+
createGuardWatcher,
|
|
135
|
+
checkDirectory,
|
|
136
|
+
getPreset,
|
|
137
|
+
listPresets
|
|
138
|
+
} from "@mandujs/core/guard";
|
|
139
|
+
|
|
140
|
+
// One-time check
|
|
141
|
+
const report = await checkDirectory(
|
|
142
|
+
{ preset: "mandu" },
|
|
143
|
+
process.cwd()
|
|
144
|
+
);
|
|
145
|
+
console.log(`Violations: ${report.totalViolations}`);
|
|
146
|
+
|
|
147
|
+
// Real-time watching
|
|
148
|
+
const watcher = createGuardWatcher({
|
|
149
|
+
config: { preset: "mandu", srcDir: "src" },
|
|
150
|
+
rootDir: process.cwd(),
|
|
151
|
+
onViolation: (v) => console.log(`${v.filePath}: ${v.ruleDescription}`),
|
|
152
|
+
});
|
|
153
|
+
watcher.start();
|
|
154
|
+
|
|
155
|
+
// List available presets
|
|
156
|
+
listPresets().forEach(p => console.log(p.name, p.description));
|
|
64
157
|
```
|
|
65
158
|
|
|
66
|
-
|
|
159
|
+
### Presets
|
|
160
|
+
|
|
161
|
+
| Preset | Layers | Use Case |
|
|
162
|
+
|--------|--------|----------|
|
|
163
|
+
| `mandu` | app, pages, widgets, features, entities, api, application, domain, infra, core, shared | Fullstack (default) |
|
|
164
|
+
| `fsd` | app, pages, widgets, features, entities, shared | Frontend |
|
|
165
|
+
| `clean` | api, application, domain, infra, shared | Backend |
|
|
166
|
+
| `hexagonal` | adapters, ports, application, domain | DDD |
|
|
167
|
+
| `atomic` | pages, templates, organisms, molecules, atoms | UI |
|
|
67
168
|
|
|
68
|
-
|
|
169
|
+
### AST-based Analysis
|
|
69
170
|
|
|
70
171
|
```typescript
|
|
71
|
-
import {
|
|
172
|
+
import { extractImportsAST, analyzeModuleAST } from "@mandujs/core/guard";
|
|
72
173
|
|
|
73
|
-
|
|
174
|
+
// Extract imports with AST (more accurate than regex)
|
|
175
|
+
const imports = extractImportsAST(code);
|
|
176
|
+
// → [{ path: "./utils", type: "static", line: 1, namedImports: ["foo"] }]
|
|
74
177
|
|
|
75
|
-
|
|
76
|
-
|
|
178
|
+
// Full module analysis
|
|
179
|
+
const analysis = analyzeModuleAST(code, "src/features/user/api.ts");
|
|
77
180
|
```
|
|
78
181
|
|
|
79
|
-
###
|
|
182
|
+
### Statistics & Trends
|
|
80
183
|
|
|
81
184
|
```typescript
|
|
82
185
|
import {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} from "@mandujs/core";
|
|
186
|
+
createScanRecord,
|
|
187
|
+
addScanRecord,
|
|
188
|
+
analyzeTrend,
|
|
189
|
+
generateGuardMarkdownReport
|
|
190
|
+
} from "@mandujs/core/guard";
|
|
191
|
+
|
|
192
|
+
// Save scan for trend analysis
|
|
193
|
+
const record = createScanRecord(report, "mandu");
|
|
194
|
+
await addScanRecord(rootDir, record);
|
|
195
|
+
|
|
196
|
+
// Analyze improvement trend
|
|
197
|
+
const trend = analyzeTrend(records, 7); // 7 days
|
|
198
|
+
console.log(trend.trend); // "improving" | "stable" | "degrading"
|
|
199
|
+
|
|
200
|
+
// Generate reports
|
|
201
|
+
const markdown = generateGuardMarkdownReport(report, trend);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
88
205
|
|
|
89
|
-
|
|
90
|
-
|
|
206
|
+
## Filling API
|
|
207
|
+
|
|
208
|
+
Handler chain for business logic.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { Mandu } from "@mandujs/core";
|
|
212
|
+
|
|
213
|
+
export default Mandu.filling()
|
|
214
|
+
// Lifecycle hooks
|
|
215
|
+
.onRequest((ctx) => {
|
|
216
|
+
ctx.set("requestId", crypto.randomUUID());
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Guard (return Response to block)
|
|
220
|
+
.guard((ctx) => {
|
|
221
|
+
if (!ctx.get("user")) return ctx.unauthorized("Login required");
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Handlers
|
|
225
|
+
.get(async (ctx) => {
|
|
226
|
+
return ctx.ok({ users: await fetchUsers() });
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
.post(async (ctx) => {
|
|
230
|
+
const body = await ctx.body();
|
|
231
|
+
return ctx.created({ user: await createUser(body) });
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// After response
|
|
235
|
+
.afterResponse((ctx) => {
|
|
236
|
+
console.log("Request completed:", ctx.get("requestId"));
|
|
237
|
+
});
|
|
238
|
+
```
|
|
91
239
|
|
|
92
|
-
|
|
93
|
-
const codeWithSlot = generateApiHandlerWithSlot(route);
|
|
240
|
+
### Middleware (Compose-style)
|
|
94
241
|
|
|
95
|
-
|
|
96
|
-
|
|
242
|
+
```typescript
|
|
243
|
+
export default Mandu.filling()
|
|
244
|
+
.middleware(async (ctx, next) => {
|
|
245
|
+
console.log("before");
|
|
246
|
+
await next();
|
|
247
|
+
console.log("after");
|
|
248
|
+
})
|
|
249
|
+
.get((ctx) => ctx.ok({ ok: true }));
|
|
97
250
|
```
|
|
98
251
|
|
|
99
|
-
|
|
252
|
+
### Context API
|
|
253
|
+
|
|
254
|
+
| Method | Description |
|
|
255
|
+
|--------|-------------|
|
|
256
|
+
| `ctx.ok(data)` | 200 OK |
|
|
257
|
+
| `ctx.created(data)` | 201 Created |
|
|
258
|
+
| `ctx.noContent()` | 204 No Content |
|
|
259
|
+
| `ctx.error(message)` | 400 Bad Request |
|
|
260
|
+
| `ctx.unauthorized(message)` | 401 Unauthorized |
|
|
261
|
+
| `ctx.forbidden(message)` | 403 Forbidden |
|
|
262
|
+
| `ctx.notFound(message)` | 404 Not Found |
|
|
263
|
+
| `ctx.fail(message)` | 500 Internal Server Error |
|
|
264
|
+
| `ctx.body<T>()` | Parse request body |
|
|
265
|
+
| `ctx.params` | Route parameters |
|
|
266
|
+
| `ctx.query` | Query parameters |
|
|
267
|
+
| `ctx.set(key, value)` | Store in context |
|
|
268
|
+
| `ctx.get<T>(key)` | Retrieve from context |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Contract API
|
|
100
273
|
|
|
101
|
-
|
|
274
|
+
Type-safe API contracts with Zod.
|
|
102
275
|
|
|
103
276
|
```typescript
|
|
104
|
-
import {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
277
|
+
import { Mandu } from "@mandujs/core";
|
|
278
|
+
import { z } from "zod";
|
|
279
|
+
|
|
280
|
+
// Define contract
|
|
281
|
+
const userContract = Mandu.contract({
|
|
282
|
+
request: {
|
|
283
|
+
GET: { query: z.object({ id: z.string() }) },
|
|
284
|
+
POST: { body: z.object({ name: z.string() }) }
|
|
285
|
+
},
|
|
286
|
+
response: {
|
|
287
|
+
200: z.object({ data: z.any() }),
|
|
288
|
+
400: z.object({ error: z.string() })
|
|
289
|
+
}
|
|
290
|
+
});
|
|
110
291
|
|
|
111
|
-
//
|
|
112
|
-
const
|
|
292
|
+
// Create typed handlers
|
|
293
|
+
const handlers = Mandu.handler(userContract, {
|
|
294
|
+
GET: (ctx) => ({ data: fetchUser(ctx.query.id) }),
|
|
295
|
+
POST: (ctx) => ({ data: createUser(ctx.body) })
|
|
296
|
+
});
|
|
113
297
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
298
|
+
// Type-safe client
|
|
299
|
+
const client = Mandu.client(userContract, { baseUrl: "/api/users" });
|
|
300
|
+
const result = await client.GET({ query: { id: "123" } });
|
|
301
|
+
```
|
|
118
302
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Runtime
|
|
306
|
+
|
|
307
|
+
Server and SSR.
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { startServer, registerApiHandler, registerPageLoader } from "@mandujs/core";
|
|
311
|
+
|
|
312
|
+
// Register handlers
|
|
313
|
+
registerApiHandler("getUsers", async (req) => ({ users: [] }));
|
|
314
|
+
registerPageLoader("home", () => import("./pages/Home"));
|
|
315
|
+
|
|
316
|
+
// Start server
|
|
317
|
+
const server = startServer(manifest, { port: 3000 });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Streaming SSR
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { renderToStream } from "@mandujs/core";
|
|
324
|
+
|
|
325
|
+
const stream = await renderToStream(<App />, {
|
|
326
|
+
bootstrapScripts: ["/client.js"],
|
|
327
|
+
onError: (err) => console.error(err)
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Client (Islands & Router)
|
|
334
|
+
|
|
335
|
+
### Island Hydration
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { createIsland, partial } from "@mandujs/core/client";
|
|
339
|
+
|
|
340
|
+
// Define island
|
|
341
|
+
const CounterIsland = createIsland({
|
|
342
|
+
name: "counter",
|
|
343
|
+
component: Counter,
|
|
344
|
+
priority: "visible"
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Partial (smaller than island)
|
|
348
|
+
const ButtonPartial = partial("submit-btn", SubmitButton);
|
|
124
349
|
```
|
|
125
350
|
|
|
126
|
-
###
|
|
351
|
+
### Client Router
|
|
127
352
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
353
|
+
```typescript
|
|
354
|
+
import { useRouter, useParams, Link, NavLink } from "@mandujs/core/client";
|
|
355
|
+
|
|
356
|
+
function Navigation() {
|
|
357
|
+
const router = useRouter();
|
|
358
|
+
const params = useParams();
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<nav>
|
|
362
|
+
<NavLink href="/users" activeClass="active">Users</NavLink>
|
|
363
|
+
<button onClick={() => router.push("/settings")}>Settings</button>
|
|
364
|
+
</nav>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
```
|
|
135
368
|
|
|
136
|
-
|
|
369
|
+
---
|
|
137
370
|
|
|
138
|
-
|
|
371
|
+
## Brain (AI Assistant)
|
|
372
|
+
|
|
373
|
+
Doctor and architecture analyzer.
|
|
139
374
|
|
|
140
375
|
```typescript
|
|
141
376
|
import {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
377
|
+
initializeBrain,
|
|
378
|
+
getBrain,
|
|
379
|
+
analyzeViolations,
|
|
380
|
+
initializeArchitectureAnalyzer
|
|
145
381
|
} from "@mandujs/core";
|
|
146
382
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
383
|
+
// Initialize
|
|
384
|
+
await initializeBrain();
|
|
385
|
+
const brain = getBrain();
|
|
386
|
+
|
|
387
|
+
// Analyze violations with suggestions
|
|
388
|
+
const analysis = await analyzeViolations(violations, { useLLM: true });
|
|
389
|
+
console.log(analysis.patches); // Suggested fixes
|
|
390
|
+
|
|
391
|
+
// Architecture analyzer
|
|
392
|
+
const analyzer = initializeArchitectureAnalyzer(rootDir);
|
|
393
|
+
const locationResult = await analyzer.checkLocation({ path: "src/features/user.ts" });
|
|
394
|
+
const importResult = await analyzer.checkImports({
|
|
395
|
+
sourceFile: "src/features/user.ts",
|
|
396
|
+
imports: ["../entities/product"]
|
|
150
397
|
});
|
|
398
|
+
```
|
|
151
399
|
|
|
152
|
-
|
|
153
|
-
registerPageLoader("homePage", () => import("./pages/Home"));
|
|
400
|
+
---
|
|
154
401
|
|
|
155
|
-
|
|
156
|
-
|
|
402
|
+
## Bundler
|
|
403
|
+
|
|
404
|
+
Client bundling with HMR.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { buildClientBundle, createDevBundler } from "@mandujs/core/bundler";
|
|
157
408
|
|
|
158
|
-
//
|
|
159
|
-
|
|
409
|
+
// Production build
|
|
410
|
+
const result = await buildClientBundle(manifest, {
|
|
411
|
+
outDir: ".mandu/client",
|
|
412
|
+
minify: true,
|
|
413
|
+
sourcemap: true
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Development with HMR
|
|
417
|
+
const devBundler = await createDevBundler(manifest, {
|
|
418
|
+
rootDir: process.cwd(),
|
|
419
|
+
isDev: true
|
|
420
|
+
});
|
|
160
421
|
```
|
|
161
422
|
|
|
162
|
-
|
|
423
|
+
---
|
|
163
424
|
|
|
164
|
-
|
|
425
|
+
## Transaction API
|
|
426
|
+
|
|
427
|
+
Atomic changes with rollback.
|
|
165
428
|
|
|
166
429
|
```typescript
|
|
167
|
-
import {
|
|
430
|
+
import { beginChange, commitChange, rollbackChange } from "@mandujs/core";
|
|
431
|
+
|
|
432
|
+
// Start transaction
|
|
433
|
+
const { changeId, snapshotId } = await beginChange(rootDir, "Add user API");
|
|
434
|
+
|
|
435
|
+
// Make changes...
|
|
168
436
|
|
|
169
|
-
|
|
170
|
-
|
|
437
|
+
// Commit or rollback
|
|
438
|
+
await commitChange(rootDir);
|
|
439
|
+
// or
|
|
440
|
+
await rollbackChange(rootDir);
|
|
171
441
|
```
|
|
172
442
|
|
|
443
|
+
---
|
|
444
|
+
|
|
173
445
|
## Types
|
|
174
446
|
|
|
175
447
|
```typescript
|
|
176
448
|
import type {
|
|
449
|
+
// Spec
|
|
177
450
|
RoutesManifest,
|
|
178
451
|
RouteSpec,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
452
|
+
|
|
453
|
+
// Guard
|
|
454
|
+
GuardPreset,
|
|
455
|
+
GuardConfig,
|
|
456
|
+
Violation,
|
|
457
|
+
ViolationReport,
|
|
458
|
+
|
|
459
|
+
// Router
|
|
460
|
+
ScanResult,
|
|
461
|
+
FSRouteConfig,
|
|
462
|
+
|
|
463
|
+
// Contract
|
|
464
|
+
ContractDefinition,
|
|
465
|
+
ContractHandlers,
|
|
466
|
+
|
|
467
|
+
// Filling
|
|
468
|
+
ManduContext,
|
|
185
469
|
} from "@mandujs/core";
|
|
186
470
|
```
|
|
187
471
|
|
|
472
|
+
---
|
|
473
|
+
|
|
188
474
|
## Requirements
|
|
189
475
|
|
|
190
476
|
- Bun >= 1.0.0
|
|
@@ -194,6 +480,7 @@ import type {
|
|
|
194
480
|
## Related Packages
|
|
195
481
|
|
|
196
482
|
- [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
|
|
483
|
+
- [@mandujs/mcp](https://www.npmjs.com/package/@mandujs/mcp) - MCP server for AI agents
|
|
197
484
|
|
|
198
485
|
## License
|
|
199
486
|
|
package/package.json
CHANGED
|
@@ -222,9 +222,9 @@ export function generateJsonReport(analysis: DoctorAnalysis): string {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
|
-
* Generate a Markdown report
|
|
225
|
+
* Generate a Markdown report (Doctor Analysis)
|
|
226
226
|
*/
|
|
227
|
-
export function
|
|
227
|
+
export function generateDoctorMarkdownReport(analysis: DoctorAnalysis): string {
|
|
228
228
|
const lines: string[] = [];
|
|
229
229
|
|
|
230
230
|
lines.push("# 🩺 Mandu Doctor Report");
|
|
@@ -328,7 +328,7 @@ export function formatDoctorReport(
|
|
|
328
328
|
return generateJsonReport(analysis);
|
|
329
329
|
|
|
330
330
|
case "markdown":
|
|
331
|
-
return
|
|
331
|
+
return generateDoctorMarkdownReport(analysis);
|
|
332
332
|
|
|
333
333
|
default:
|
|
334
334
|
printDoctorReport(analysis);
|
package/src/guard/index.ts
CHANGED
package/src/guard/statistics.ts
CHANGED
|
@@ -360,9 +360,9 @@ export async function addScanRecord(
|
|
|
360
360
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
361
361
|
|
|
362
362
|
/**
|
|
363
|
-
* 마크다운 리포트 생성
|
|
363
|
+
* 마크다운 리포트 생성 (Guard Report)
|
|
364
364
|
*/
|
|
365
|
-
export function
|
|
365
|
+
export function generateGuardMarkdownReport(
|
|
366
366
|
report: ViolationReport,
|
|
367
367
|
trend?: TrendAnalysis | null,
|
|
368
368
|
layerStats?: LayerStatistics[]
|
|
@@ -499,7 +499,7 @@ export function generateHTMLReport(
|
|
|
499
499
|
trend?: TrendAnalysis | null,
|
|
500
500
|
layerStats?: LayerStatistics[]
|
|
501
501
|
): string {
|
|
502
|
-
const markdown =
|
|
502
|
+
const markdown = generateGuardMarkdownReport(report, trend, layerStats);
|
|
503
503
|
|
|
504
504
|
// 간단한 마크다운 → HTML 변환
|
|
505
505
|
let html = markdown
|
package/src/guard/types.ts
CHANGED
|
@@ -84,6 +84,9 @@ export interface GuardConfig {
|
|
|
84
84
|
/** FS Routes 통합 */
|
|
85
85
|
fsRoutes?: FSRoutesGuardConfig;
|
|
86
86
|
|
|
87
|
+
/** 실시간 출력 형식 */
|
|
88
|
+
realtimeOutput?: "console" | "agent" | "json";
|
|
89
|
+
|
|
87
90
|
/** 캐시 사용 여부 (기본값: true) */
|
|
88
91
|
cache?: boolean;
|
|
89
92
|
|
|
@@ -320,6 +323,7 @@ export const DEFAULT_GUARD_CONFIG: Required<Omit<GuardConfig, "preset" | "layers
|
|
|
320
323
|
deepNesting: "info",
|
|
321
324
|
crossSliceDependency: "warn",
|
|
322
325
|
},
|
|
326
|
+
realtimeOutput: "console",
|
|
323
327
|
cache: true,
|
|
324
328
|
incremental: true,
|
|
325
329
|
debounceMs: 100,
|
package/src/guard/watcher.ts
CHANGED
|
@@ -20,7 +20,11 @@ import type {
|
|
|
20
20
|
import { WATCH_EXTENSIONS, DEFAULT_GUARD_CONFIG } from "./types";
|
|
21
21
|
import { analyzeFile, shouldAnalyzeFile } from "./analyzer";
|
|
22
22
|
import { validateFileAnalysis, detectCircularDependencies } from "./validator";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
printRealtimeViolation,
|
|
25
|
+
formatViolationForAgent,
|
|
26
|
+
formatViolationAsAgentJSON,
|
|
27
|
+
} from "./reporter";
|
|
24
28
|
import { getPreset } from "./presets";
|
|
25
29
|
|
|
26
30
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -119,12 +123,24 @@ export function createGuardWatcher(options: WatcherOptions): GuardWatcher {
|
|
|
119
123
|
// 콜백 호출
|
|
120
124
|
onFileAnalyzed?.(analysis, violations);
|
|
121
125
|
|
|
126
|
+
const realtimeOutput = config.realtimeOutput ?? DEFAULT_GUARD_CONFIG.realtimeOutput;
|
|
127
|
+
|
|
122
128
|
// 위반 처리
|
|
123
129
|
for (const violation of violations) {
|
|
124
130
|
onViolation?.(violation);
|
|
125
131
|
|
|
126
132
|
if (!silent) {
|
|
127
|
-
|
|
133
|
+
switch (realtimeOutput) {
|
|
134
|
+
case "agent":
|
|
135
|
+
console.log(formatViolationForAgent(violation, config.preset));
|
|
136
|
+
break;
|
|
137
|
+
case "json":
|
|
138
|
+
console.log(formatViolationAsAgentJSON(violation, config.preset));
|
|
139
|
+
break;
|
|
140
|
+
case "console":
|
|
141
|
+
default:
|
|
142
|
+
printRealtimeViolation(violation);
|
|
143
|
+
}
|
|
128
144
|
}
|
|
129
145
|
}
|
|
130
146
|
} catch (error) {
|
|
@@ -239,7 +255,8 @@ export function createGuardWatcher(options: WatcherOptions): GuardWatcher {
|
|
|
239
255
|
watcher.on("change", (path) => handleFileChange("change", resolve(rootDir, path)));
|
|
240
256
|
watcher.on("unlink", (path) => handleFileChange("unlink", resolve(rootDir, path)));
|
|
241
257
|
|
|
242
|
-
|
|
258
|
+
const realtimeOutput = config.realtimeOutput ?? DEFAULT_GUARD_CONFIG.realtimeOutput;
|
|
259
|
+
if (!silent && realtimeOutput === "console") {
|
|
243
260
|
console.log(`[Guard] 🛡️ Watching ${Array.from(scanRoots).join(", ")} for architecture violations...`);
|
|
244
261
|
}
|
|
245
262
|
},
|