@mandujs/core 0.18.22 → 0.19.2

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.
Files changed (91) hide show
  1. package/README.ko.md +0 -14
  2. package/package.json +4 -1
  3. package/src/brain/architecture/analyzer.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +18 -14
  5. package/src/bundler/build.test.ts +127 -0
  6. package/src/bundler/build.ts +291 -113
  7. package/src/bundler/css.ts +20 -5
  8. package/src/bundler/dev.ts +55 -2
  9. package/src/bundler/prerender.ts +195 -0
  10. package/src/change/snapshot.ts +4 -23
  11. package/src/change/types.ts +2 -3
  12. package/src/client/Form.tsx +105 -0
  13. package/src/client/__tests__/use-sse.test.ts +153 -0
  14. package/src/client/hooks.ts +105 -6
  15. package/src/client/index.ts +35 -6
  16. package/src/client/router.ts +670 -433
  17. package/src/client/rpc.ts +140 -0
  18. package/src/client/runtime.ts +24 -21
  19. package/src/client/use-fetch.ts +239 -0
  20. package/src/client/use-head.ts +197 -0
  21. package/src/client/use-sse.ts +378 -0
  22. package/src/components/Image.tsx +162 -0
  23. package/src/config/mandu.ts +5 -0
  24. package/src/config/validate.ts +34 -0
  25. package/src/content/index.ts +5 -1
  26. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  27. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  28. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  29. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  30. package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
  31. package/src/devtools/client/components/panel/index.ts +45 -32
  32. package/src/devtools/client/components/panel/panel-container.tsx +332 -312
  33. package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
  34. package/src/devtools/client/state-manager.ts +535 -478
  35. package/src/devtools/design-tokens.ts +265 -264
  36. package/src/devtools/types.ts +345 -319
  37. package/src/filling/context.ts +65 -0
  38. package/src/filling/filling.ts +336 -14
  39. package/src/filling/index.ts +5 -1
  40. package/src/filling/session.ts +216 -0
  41. package/src/filling/ws.ts +78 -0
  42. package/src/generator/generate.ts +2 -2
  43. package/src/guard/auto-correct.ts +0 -29
  44. package/src/guard/check.ts +14 -31
  45. package/src/guard/presets/index.ts +296 -294
  46. package/src/guard/rules.ts +15 -19
  47. package/src/guard/validator.ts +834 -834
  48. package/src/index.ts +5 -1
  49. package/src/island/index.ts +373 -304
  50. package/src/kitchen/api/contract-api.ts +225 -0
  51. package/src/kitchen/api/diff-parser.ts +108 -0
  52. package/src/kitchen/api/file-api.ts +273 -0
  53. package/src/kitchen/api/guard-api.ts +83 -0
  54. package/src/kitchen/api/guard-decisions.ts +100 -0
  55. package/src/kitchen/api/routes-api.ts +50 -0
  56. package/src/kitchen/index.ts +21 -0
  57. package/src/kitchen/kitchen-handler.ts +256 -0
  58. package/src/kitchen/kitchen-ui.ts +1732 -0
  59. package/src/kitchen/stream/activity-sse.ts +145 -0
  60. package/src/kitchen/stream/file-tailer.ts +99 -0
  61. package/src/middleware/compress.ts +62 -0
  62. package/src/middleware/cors.ts +47 -0
  63. package/src/middleware/index.ts +10 -0
  64. package/src/middleware/jwt.ts +134 -0
  65. package/src/middleware/logger.ts +58 -0
  66. package/src/middleware/timeout.ts +55 -0
  67. package/src/paths.ts +0 -4
  68. package/src/plugins/hooks.ts +64 -0
  69. package/src/plugins/index.ts +3 -0
  70. package/src/plugins/types.ts +5 -0
  71. package/src/report/build.ts +0 -6
  72. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  73. package/src/router/fs-patterns.ts +11 -1
  74. package/src/router/fs-routes.ts +78 -14
  75. package/src/router/fs-scanner.ts +2 -2
  76. package/src/router/fs-types.ts +2 -1
  77. package/src/runtime/adapter-bun.ts +62 -0
  78. package/src/runtime/adapter.ts +47 -0
  79. package/src/runtime/cache.ts +310 -0
  80. package/src/runtime/handler.ts +65 -0
  81. package/src/runtime/image-handler.ts +195 -0
  82. package/src/runtime/index.ts +12 -0
  83. package/src/runtime/middleware.ts +263 -0
  84. package/src/runtime/server.ts +686 -92
  85. package/src/runtime/ssr.ts +55 -29
  86. package/src/runtime/streaming-ssr.ts +106 -82
  87. package/src/spec/index.ts +0 -1
  88. package/src/spec/schema.ts +1 -0
  89. package/src/testing/index.ts +144 -0
  90. package/src/watcher/watcher.ts +27 -1
  91. package/src/spec/lock.ts +0 -56
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Guard Decision Manager
3
+ *
4
+ * Persists approve/reject decisions for guard violations.
5
+ * Stored in .mandu/guard-decisions.json.
6
+ */
7
+
8
+ import path from "path";
9
+ import fs from "fs";
10
+
11
+ export interface GuardDecision {
12
+ id: string;
13
+ violationKey: string; // "${ruleId}::${filePath}"
14
+ action: "approve" | "reject";
15
+ ruleId: string;
16
+ filePath: string;
17
+ reason?: string;
18
+ decidedAt: string;
19
+ }
20
+
21
+ export class GuardDecisionManager {
22
+ private filePath: string;
23
+ private decisions: GuardDecision[] | null = null;
24
+
25
+ constructor(private rootDir: string) {
26
+ this.filePath = path.join(rootDir, ".mandu", "guard-decisions.json");
27
+ }
28
+
29
+ async load(): Promise<GuardDecision[]> {
30
+ if (this.decisions !== null) return this.decisions;
31
+
32
+ try {
33
+ const file = Bun.file(this.filePath);
34
+ if (await file.exists()) {
35
+ const text = await file.text();
36
+ this.decisions = JSON.parse(text);
37
+ return this.decisions!;
38
+ }
39
+ } catch {
40
+ // File doesn't exist or is corrupt
41
+ }
42
+ this.decisions = [];
43
+ return this.decisions;
44
+ }
45
+
46
+ async save(
47
+ decision: Omit<GuardDecision, "id" | "decidedAt">,
48
+ ): Promise<GuardDecision> {
49
+ const decisions = await this.load();
50
+
51
+ const full: GuardDecision = {
52
+ ...decision,
53
+ id: generateId(),
54
+ decidedAt: new Date().toISOString(),
55
+ };
56
+
57
+ // Replace existing decision for same violationKey
58
+ const idx = decisions.findIndex(
59
+ (d) => d.violationKey === full.violationKey,
60
+ );
61
+ if (idx >= 0) {
62
+ decisions[idx] = full;
63
+ } else {
64
+ decisions.push(full);
65
+ }
66
+
67
+ this.decisions = decisions;
68
+ await this.persist();
69
+ return full;
70
+ }
71
+
72
+ async remove(id: string): Promise<boolean> {
73
+ const decisions = await this.load();
74
+ const idx = decisions.findIndex((d) => d.id === id);
75
+ if (idx < 0) return false;
76
+
77
+ decisions.splice(idx, 1);
78
+ this.decisions = decisions;
79
+ await this.persist();
80
+ return true;
81
+ }
82
+
83
+ async isApproved(ruleId: string, filePath: string): Promise<boolean> {
84
+ const decisions = await this.load();
85
+ const key = `${ruleId}::${filePath}`;
86
+ return decisions.some((d) => d.violationKey === key && d.action === "approve");
87
+ }
88
+
89
+ private async persist(): Promise<void> {
90
+ const dir = path.dirname(this.filePath);
91
+ if (!fs.existsSync(dir)) {
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ }
94
+ await Bun.write(this.filePath, JSON.stringify(this.decisions, null, 2));
95
+ }
96
+ }
97
+
98
+ function generateId(): string {
99
+ return `gd_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
100
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Routes API - Exposes RoutesManifest data to Kitchen UI.
3
+ *
4
+ * GET /__kitchen/api/routes → JSON list of all registered routes.
5
+ */
6
+
7
+ import type { RoutesManifest, RouteSpec } from "../../spec/schema";
8
+
9
+ export interface RouteInfo {
10
+ id: string;
11
+ pattern: string;
12
+ kind: "page" | "api";
13
+ module: string;
14
+ methods?: string[];
15
+ hasSlot: boolean;
16
+ hasContract: boolean;
17
+ hasClient: boolean;
18
+ hasLayout: boolean;
19
+ hydration?: string;
20
+ }
21
+
22
+ function toRouteInfo(route: RouteSpec): RouteInfo {
23
+ return {
24
+ id: route.id,
25
+ pattern: route.pattern,
26
+ kind: route.kind,
27
+ module: route.module,
28
+ methods: route.methods,
29
+ hasSlot: !!route.slotModule,
30
+ hasContract: !!route.contractModule,
31
+ hasClient: !!route.clientModule,
32
+ hasLayout: !!(route.kind === "page" && route.layoutChain?.length),
33
+ hydration: route.kind === "page" ? route.hydration?.strategy : undefined,
34
+ };
35
+ }
36
+
37
+ export function handleRoutesRequest(manifest: RoutesManifest): Response {
38
+ const routes = manifest.routes.map(toRouteInfo);
39
+
40
+ const summary = {
41
+ total: routes.length,
42
+ pages: routes.filter((r) => r.kind === "page").length,
43
+ apis: routes.filter((r) => r.kind === "api").length,
44
+ withSlots: routes.filter((r) => r.hasSlot).length,
45
+ withContracts: routes.filter((r) => r.hasContract).length,
46
+ withIslands: routes.filter((r) => r.hasClient).length,
47
+ };
48
+
49
+ return Response.json({ routes, summary });
50
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Kitchen Module - Dev-only dashboard for Mandu projects.
3
+ *
4
+ * Provides real-time MCP activity stream, route explorer,
5
+ * and architecture guard dashboard at /__kitchen.
6
+ */
7
+
8
+ export { KitchenHandler, KITCHEN_PREFIX, getKitchenErrors, clearKitchenErrors } from "./kitchen-handler";
9
+ export type { KitchenOptions } from "./kitchen-handler";
10
+ export { ActivitySSEBroadcaster } from "./stream/activity-sse";
11
+ export { FileTailer } from "./stream/file-tailer";
12
+ export { GuardAPI } from "./api/guard-api";
13
+ export { handleRoutesRequest } from "./api/routes-api";
14
+ export { FileAPI } from "./api/file-api";
15
+ export { parseUnifiedDiff } from "./api/diff-parser";
16
+ export type { FileDiff, DiffHunk, DiffLine } from "./api/diff-parser";
17
+ export type { RecentFileChange } from "./api/file-api";
18
+ export { GuardDecisionManager } from "./api/guard-decisions";
19
+ export type { GuardDecision } from "./api/guard-decisions";
20
+ export { ContractPlaygroundAPI } from "./api/contract-api";
21
+ export type { ContractListItem } from "./api/contract-api";
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Kitchen HTTP Handler - Dispatches /__kitchen/* requests.
3
+ *
4
+ * Mounted inside handleRequestInternal() when isDev === true.
5
+ * All Kitchen routes are under /__kitchen prefix.
6
+ */
7
+
8
+ import type { RoutesManifest } from "../spec/schema";
9
+ import type { GuardConfig } from "../guard/types";
10
+ import { ActivitySSEBroadcaster } from "./stream/activity-sse";
11
+ import { GuardAPI } from "./api/guard-api";
12
+ import { handleRoutesRequest } from "./api/routes-api";
13
+ import { FileAPI } from "./api/file-api";
14
+ import { GuardDecisionManager } from "./api/guard-decisions";
15
+ import { ContractPlaygroundAPI } from "./api/contract-api";
16
+ import { renderKitchenHTML } from "./kitchen-ui";
17
+
18
+ export const KITCHEN_PREFIX = "/__kitchen";
19
+
20
+ export interface KitchenOptions {
21
+ rootDir: string;
22
+ manifest: RoutesManifest;
23
+ guardConfig: GuardConfig | null;
24
+ }
25
+
26
+ /** In-memory error store for Kitchen → MCP bridge */
27
+ interface KitchenError {
28
+ id: string;
29
+ type: string;
30
+ severity: string;
31
+ message: string;
32
+ stack?: string;
33
+ url?: string;
34
+ source?: string;
35
+ line?: number;
36
+ column?: number;
37
+ timestamp: number;
38
+ }
39
+
40
+ const MAX_STORED_ERRORS = 50;
41
+ let storedErrors: KitchenError[] = [];
42
+
43
+ /** Get stored errors (used by MCP tools) */
44
+ export function getKitchenErrors(): KitchenError[] {
45
+ return storedErrors;
46
+ }
47
+
48
+ /** Clear stored errors */
49
+ export function clearKitchenErrors(): void {
50
+ storedErrors = [];
51
+ }
52
+
53
+ export class KitchenHandler {
54
+ private sse: ActivitySSEBroadcaster;
55
+ private guardAPI: GuardAPI;
56
+ private fileAPI: FileAPI;
57
+ private guardDecisions: GuardDecisionManager;
58
+ private contractAPI: ContractPlaygroundAPI;
59
+ private manifest: RoutesManifest;
60
+
61
+ constructor(private options: KitchenOptions) {
62
+ this.manifest = options.manifest;
63
+ this.sse = new ActivitySSEBroadcaster(options.rootDir);
64
+ this.guardAPI = new GuardAPI(options.guardConfig, options.rootDir);
65
+ this.fileAPI = new FileAPI(options.rootDir);
66
+ this.guardDecisions = new GuardDecisionManager(options.rootDir);
67
+ this.contractAPI = new ContractPlaygroundAPI(options.manifest, options.rootDir);
68
+ }
69
+
70
+ start(): void {
71
+ this.sse.start();
72
+ }
73
+
74
+ stop(): void {
75
+ this.sse.stop();
76
+ }
77
+
78
+ /** Update manifest when routes change (HMR rebuild) */
79
+ updateManifest(manifest: RoutesManifest): void {
80
+ this.manifest = manifest;
81
+ this.contractAPI.updateManifest(manifest);
82
+ }
83
+
84
+ /** Update guard config when mandu.config.ts changes */
85
+ updateGuardConfig(config: GuardConfig | null): void {
86
+ this.guardAPI.updateConfig(config);
87
+ }
88
+
89
+ /** Get the SSE broadcaster for external event injection */
90
+ get broadcaster(): ActivitySSEBroadcaster {
91
+ return this.sse;
92
+ }
93
+
94
+ /** Get the Guard API for pushing violation reports */
95
+ get guard(): GuardAPI {
96
+ return this.guardAPI;
97
+ }
98
+
99
+ /**
100
+ * Handle a /__kitchen/* request.
101
+ * Returns Response or null if path doesn't match.
102
+ */
103
+ async handle(req: Request, pathname: string): Promise<Response | null> {
104
+ if (!pathname.startsWith(KITCHEN_PREFIX)) {
105
+ return null;
106
+ }
107
+
108
+ const sub = pathname.slice(KITCHEN_PREFIX.length) || "/";
109
+
110
+ // Kitchen dashboard UI
111
+ if (sub === "/" || sub === "") {
112
+ return new Response(renderKitchenHTML(), {
113
+ headers: { "Content-Type": "text/html; charset=utf-8" },
114
+ });
115
+ }
116
+
117
+ // SSE activity stream
118
+ if (sub === "/sse/activity") {
119
+ return this.sse.createResponse();
120
+ }
121
+
122
+ // Routes API
123
+ if (sub === "/api/routes") {
124
+ return handleRoutesRequest(this.manifest);
125
+ }
126
+
127
+ // Guard API
128
+ if (sub === "/api/guard" && req.method === "GET") {
129
+ return this.guardAPI.handleGetReport();
130
+ }
131
+
132
+ if (sub === "/api/guard/scan" && req.method === "POST") {
133
+ return this.guardAPI.handleScan();
134
+ }
135
+
136
+ // Guard Decisions API
137
+ if (sub === "/api/guard/decisions" && req.method === "GET") {
138
+ const decisions = await this.guardDecisions.load();
139
+ return Response.json({ decisions });
140
+ }
141
+
142
+ if (sub === "/api/guard/approve" && req.method === "POST") {
143
+ try {
144
+ const body = await req.json();
145
+ const decision = await this.guardDecisions.save({
146
+ violationKey: `${body.ruleId}::${body.filePath}`,
147
+ action: "approve",
148
+ ruleId: body.ruleId,
149
+ filePath: body.filePath,
150
+ reason: body.reason,
151
+ });
152
+ return Response.json({ decision });
153
+ } catch {
154
+ return Response.json({ error: "Invalid request body" }, { status: 400 });
155
+ }
156
+ }
157
+
158
+ if (sub === "/api/guard/reject" && req.method === "POST") {
159
+ try {
160
+ const body = await req.json();
161
+ const decision = await this.guardDecisions.save({
162
+ violationKey: `${body.ruleId}::${body.filePath}`,
163
+ action: "reject",
164
+ ruleId: body.ruleId,
165
+ filePath: body.filePath,
166
+ reason: body.reason,
167
+ });
168
+ return Response.json({ decision });
169
+ } catch {
170
+ return Response.json({ error: "Invalid request body" }, { status: 400 });
171
+ }
172
+ }
173
+
174
+ if (sub.startsWith("/api/guard/decisions/") && req.method === "DELETE") {
175
+ const id = sub.slice("/api/guard/decisions/".length);
176
+ const removed = await this.guardDecisions.remove(id);
177
+ if (!removed) {
178
+ return Response.json({ error: "Decision not found" }, { status: 404 });
179
+ }
180
+ return Response.json({ removed: true });
181
+ }
182
+
183
+ // Error API (Kitchen → MCP bridge)
184
+ if (sub === "/api/errors" && req.method === "POST") {
185
+ try {
186
+ const body = await req.json() as KitchenError | KitchenError[];
187
+ const errors = Array.isArray(body) ? body : [body];
188
+ for (const error of errors) {
189
+ if (!error.message) continue;
190
+ error.timestamp = error.timestamp || Date.now();
191
+ error.id = error.id || `err_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
192
+ storedErrors.push(error);
193
+ if (storedErrors.length > MAX_STORED_ERRORS) {
194
+ storedErrors.shift();
195
+ }
196
+ }
197
+ return Response.json({ received: errors.length, total: storedErrors.length });
198
+ } catch {
199
+ return Response.json({ error: "Invalid error payload" }, { status: 400 });
200
+ }
201
+ }
202
+
203
+ if (sub === "/api/errors" && req.method === "GET") {
204
+ return Response.json({ errors: storedErrors, count: storedErrors.length });
205
+ }
206
+
207
+ if (sub === "/api/errors" && req.method === "DELETE") {
208
+ const count = storedErrors.length;
209
+ clearKitchenErrors();
210
+ return Response.json({ cleared: count });
211
+ }
212
+
213
+ // File API
214
+ if (sub === "/api/file" && req.method === "GET") {
215
+ return this.fileAPI.handleReadFile(new URL(req.url));
216
+ }
217
+
218
+ if (sub === "/api/file/diff" && req.method === "GET") {
219
+ return this.fileAPI.handleFileDiff(new URL(req.url));
220
+ }
221
+
222
+ if (sub === "/api/file/changes" && req.method === "GET") {
223
+ return this.fileAPI.handleRecentChanges();
224
+ }
225
+
226
+ // Contract API
227
+ if (sub === "/api/contracts" && req.method === "GET") {
228
+ return this.contractAPI.handleList();
229
+ }
230
+
231
+ if (sub === "/api/contracts/validate" && req.method === "POST") {
232
+ return this.contractAPI.handleValidate(req);
233
+ }
234
+
235
+ if (sub === "/api/contracts/openapi" && req.method === "GET") {
236
+ return this.contractAPI.handleOpenAPI();
237
+ }
238
+
239
+ if (sub === "/api/contracts/openapi.yaml" && req.method === "GET") {
240
+ return this.contractAPI.handleOpenAPIYAML();
241
+ }
242
+
243
+ if (sub.startsWith("/api/contracts/") && req.method === "GET") {
244
+ const id = sub.slice("/api/contracts/".length);
245
+ if (id && !id.includes("/")) {
246
+ return this.contractAPI.handleDetail(id);
247
+ }
248
+ }
249
+
250
+ // Unknown kitchen route
251
+ return Response.json(
252
+ { error: "Not found", path: pathname },
253
+ { status: 404 },
254
+ );
255
+ }
256
+ }