@mandujs/mcp 0.18.9 → 0.18.10
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 +0 -1
- package/package.json +42 -42
- package/src/activity-monitor.ts +9 -8
- package/src/adapters/tool-adapter.ts +2 -0
- package/src/executor/error-handler.ts +268 -250
- package/src/index.ts +8 -0
- package/src/new-resources.ts +119 -0
- package/src/profiles.ts +34 -0
- package/src/prompts.ts +104 -0
- package/src/resources/handlers.ts +0 -23
- package/src/server.ts +78 -5
- package/src/tools/ate.ts +28 -0
- package/src/tools/brain.ts +56 -24
- package/src/tools/component.ts +194 -185
- package/src/tools/composite.ts +440 -0
- package/src/tools/contract.ts +58 -58
- package/src/tools/decisions.ts +270 -0
- package/src/tools/generate.ts +23 -21
- package/src/tools/guard.ts +32 -708
- package/src/tools/history.ts +24 -7
- package/src/tools/hydration.ts +40 -13
- package/src/tools/index.ts +28 -2
- package/src/tools/kitchen.ts +107 -0
- package/src/tools/negotiate.ts +263 -0
- package/src/tools/project.ts +464 -382
- package/src/tools/resource.ts +19 -2
- package/src/tools/runtime.ts +533 -508
- package/src/tools/seo.ts +446 -417
- package/src/tools/slot-validation.ts +200 -0
- package/src/tools/slot.ts +20 -25
- package/src/tools/spec.ts +45 -43
- package/src/tools/transaction.ts +55 -13
- package/src/tx-lock.ts +73 -0
- package/src/utils/project.ts +48 -9
- package/src/utils/runtime-control.ts +52 -0
- package/src/utils/withWarnings.ts +2 -1
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu MCP - Composite Tools
|
|
3
|
+
* Multi-step workflow tools combining existing handlers into single-call operations.
|
|
4
|
+
*/
|
|
5
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { getProjectPaths } from "../utils/project.js";
|
|
7
|
+
import { specTools } from "./spec.js";
|
|
8
|
+
import { guardTools } from "./guard.js";
|
|
9
|
+
import { negotiateTools } from "./negotiate.js";
|
|
10
|
+
import { contractTools } from "./contract.js";
|
|
11
|
+
import { generateTools } from "./generate.js";
|
|
12
|
+
import { kitchenTools } from "./kitchen.js";
|
|
13
|
+
import { ateTools } from "./ate.js";
|
|
14
|
+
import { requestRuntimeCache } from "../utils/runtime-control.js";
|
|
15
|
+
import { requireLock } from "../tx-lock.js";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import fs from "fs/promises";
|
|
18
|
+
|
|
19
|
+
export const compositeToolDefinitions: Tool[] = [
|
|
20
|
+
{
|
|
21
|
+
name: "mandu.feature.create",
|
|
22
|
+
description:
|
|
23
|
+
"Create a complete feature: route + contract + slot + island scaffold in one call. " +
|
|
24
|
+
"Sequentially runs: negotiate -> add_route -> create_contract -> generate -> guard_check.",
|
|
25
|
+
annotations: {
|
|
26
|
+
destructiveHint: true,
|
|
27
|
+
readOnlyHint: false,
|
|
28
|
+
},
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
name: { type: "string", description: "Feature name (English kebab-case)" },
|
|
33
|
+
description: { type: "string", description: "Feature description" },
|
|
34
|
+
kind: { type: "string", enum: ["page", "api", "both"], description: "Route kind (default: both)" },
|
|
35
|
+
methods: { type: "array", items: { type: "string" }, description: "HTTP methods (default: ['GET', 'POST'])" },
|
|
36
|
+
withContract: { type: "boolean", description: "Create Zod contract file (default: true)" },
|
|
37
|
+
withIsland: { type: "boolean", description: "Create island component (default: false)" },
|
|
38
|
+
},
|
|
39
|
+
required: ["name", "description"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "mandu.diagnose",
|
|
44
|
+
description:
|
|
45
|
+
"Run all diagnostic checks in parallel and return a unified health report. " +
|
|
46
|
+
"Combines: kitchen_errors + guard_check + validate_contracts + validate_manifest.",
|
|
47
|
+
annotations: {
|
|
48
|
+
readOnlyHint: true,
|
|
49
|
+
},
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
autoFix: { type: "boolean", description: "Attempt automatic fixes for guard violations (default: false)" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "mandu.island.add",
|
|
59
|
+
description:
|
|
60
|
+
"Create an island component with correct @mandujs/core/client imports and hydration strategy. " +
|
|
61
|
+
"Generates a .island.tsx file in app/{route}/ with the island() wrapper.",
|
|
62
|
+
annotations: {
|
|
63
|
+
destructiveHint: true,
|
|
64
|
+
readOnlyHint: false,
|
|
65
|
+
},
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
name: { type: "string", description: "Island component name (PascalCase)" },
|
|
70
|
+
route: { type: "string", description: "Route path to attach to (e.g. 'blog/[slug]')" },
|
|
71
|
+
strategy: { type: "string", enum: ["load", "idle", "visible", "media", "never"], description: "Hydration strategy (default: visible)" },
|
|
72
|
+
},
|
|
73
|
+
required: ["name", "route"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "mandu.middleware.add",
|
|
78
|
+
description:
|
|
79
|
+
"Create a middleware.ts file from a preset template (jwt, cors, auth, default). " +
|
|
80
|
+
"Checks if middleware.ts already exists before writing to avoid overwriting.",
|
|
81
|
+
annotations: {
|
|
82
|
+
destructiveHint: false,
|
|
83
|
+
readOnlyHint: false,
|
|
84
|
+
},
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
preset: { type: "string", enum: ["jwt", "cors", "auth", "default"], description: "Middleware preset template" },
|
|
89
|
+
options: { type: "object", additionalProperties: { type: "string" }, description: "Extra options passed into the template" },
|
|
90
|
+
},
|
|
91
|
+
required: ["preset"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "mandu.test.route",
|
|
96
|
+
description:
|
|
97
|
+
"Run the ATE test pipeline on a single route: extract -> generate -> run -> report. " +
|
|
98
|
+
"Set quick=true to skip extraction and use cached data.",
|
|
99
|
+
annotations: {
|
|
100
|
+
readOnlyHint: false,
|
|
101
|
+
},
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
routeId: { type: "string", description: "Route ID to test (e.g. 'api-users', 'blog-slug')" },
|
|
106
|
+
quick: { type: "boolean", description: "Skip extraction, reuse cached graph (default: false)" },
|
|
107
|
+
},
|
|
108
|
+
required: ["routeId"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "mandu.deploy.check",
|
|
113
|
+
description:
|
|
114
|
+
"Pre-deployment validation: runs guard, contract, and manifest checks in parallel. " +
|
|
115
|
+
"Returns a structured readiness report with pass/fail per check and any blockers.",
|
|
116
|
+
annotations: {
|
|
117
|
+
readOnlyHint: true,
|
|
118
|
+
},
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
target: { type: "string", enum: ["bun", "docker", "node"], description: "Deployment target (informational, default: bun)" },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "mandu.cache.manage",
|
|
128
|
+
description:
|
|
129
|
+
"Cache management operations. 'stats' reads cache info from Kitchen endpoint. " +
|
|
130
|
+
"'clear' explains that runtime cache requires server restart or revalidatePath/Tag.",
|
|
131
|
+
annotations: {
|
|
132
|
+
readOnlyHint: false,
|
|
133
|
+
},
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
action: { type: "string", enum: ["stats", "clear"], description: "Cache operation" },
|
|
138
|
+
path: { type: "string", description: "Route path to target (for selective clear)" },
|
|
139
|
+
tag: { type: "string", description: "Cache tag to target (for tag-based clear)" },
|
|
140
|
+
},
|
|
141
|
+
required: ["action"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/** Extract an error message from an unknown thrown value. */
|
|
147
|
+
function toErrorMessage(e: unknown): string {
|
|
148
|
+
if (e instanceof Error) return e.message;
|
|
149
|
+
if (typeof e === "string") return e;
|
|
150
|
+
return String(e);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
154
|
+
return typeof value === "object" && value !== null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function compositeTools(projectRoot: string) {
|
|
158
|
+
const paths = getProjectPaths(projectRoot);
|
|
159
|
+
const spec = specTools(projectRoot);
|
|
160
|
+
const guard = guardTools(projectRoot);
|
|
161
|
+
const neg = negotiateTools(projectRoot);
|
|
162
|
+
const contract = contractTools(projectRoot);
|
|
163
|
+
const generate = generateTools(projectRoot);
|
|
164
|
+
const kitchen = kitchenTools(projectRoot);
|
|
165
|
+
const ate = ateTools(projectRoot);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"mandu.feature.create": async (args: Record<string, unknown>) => {
|
|
169
|
+
const lockCheck = requireLock(args.lockId as string | undefined);
|
|
170
|
+
if (!lockCheck.allowed) {
|
|
171
|
+
return { error: lockCheck.error, hint: "Use mandu.tx.begin to acquire a lock first" };
|
|
172
|
+
}
|
|
173
|
+
const { name, description, kind = "both", methods = ["GET", "POST"],
|
|
174
|
+
withContract = true, withIsland = false,
|
|
175
|
+
} = args as {
|
|
176
|
+
name: string; description: string; kind?: "page" | "api" | "both";
|
|
177
|
+
methods?: string[]; withContract?: boolean; withIsland?: boolean;
|
|
178
|
+
};
|
|
179
|
+
const steps: Array<{ step: string; success: boolean; result?: unknown; error?: string }> = [];
|
|
180
|
+
const kinds: Array<"page" | "api"> = kind === "both" ? ["api", "page"] : [kind];
|
|
181
|
+
|
|
182
|
+
// Step 1: negotiate architecture
|
|
183
|
+
try {
|
|
184
|
+
const result = await neg["mandu.negotiate"]({ intent: description, featureName: name });
|
|
185
|
+
steps.push({ step: "negotiate", success: true, result });
|
|
186
|
+
} catch (e) {
|
|
187
|
+
steps.push({ step: "negotiate", success: false, error: toErrorMessage(e) });
|
|
188
|
+
return { success: false, feature: name, steps, failedAt: "negotiate" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 2-3: add routes + contracts
|
|
192
|
+
for (const k of kinds) {
|
|
193
|
+
const routePath = k === "api" ? `api/${name}` : name;
|
|
194
|
+
const stepName = `add_route(${k})`;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const result = await spec["mandu.route.add"]({ path: routePath, kind: k, withSlot: true, withContract: false });
|
|
198
|
+
steps.push({ step: stepName, success: true, result });
|
|
199
|
+
} catch (e) {
|
|
200
|
+
steps.push({ step: stepName, success: false, error: toErrorMessage(e) });
|
|
201
|
+
return { success: false, feature: name, steps, failedAt: stepName };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (withContract && k === "api") {
|
|
205
|
+
const routeId = routePath.replace(/\//g, "-").replace(/[\[\]\.]/g, "");
|
|
206
|
+
try {
|
|
207
|
+
const result = await contract["mandu.contract.create"]({ routeId, description, methods });
|
|
208
|
+
steps.push({ step: "create_contract", success: true, result });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
steps.push({ step: "create_contract", success: false, error: toErrorMessage(e) });
|
|
211
|
+
return { success: false, feature: name, steps, failedAt: "create_contract" };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Step 4: generate
|
|
217
|
+
try {
|
|
218
|
+
const result = await generate["mandu.generate"]({ dryRun: false });
|
|
219
|
+
steps.push({ step: "generate", success: true, result });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
steps.push({ step: "generate", success: false, error: toErrorMessage(e) });
|
|
222
|
+
return { success: false, feature: name, steps, failedAt: "generate" };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 5: guard_check
|
|
226
|
+
try {
|
|
227
|
+
const result = await guard["mandu.guard.check"]({ autoCorrect: false });
|
|
228
|
+
steps.push({ step: "guard_check", success: true, result });
|
|
229
|
+
} catch (e) {
|
|
230
|
+
steps.push({ step: "guard_check", success: false, error: toErrorMessage(e) });
|
|
231
|
+
return { success: false, feature: name, steps, failedAt: "guard_check" };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Step 6 (optional): create island
|
|
235
|
+
if (withIsland && kinds.includes("page")) {
|
|
236
|
+
const pc = toPascalCase(name);
|
|
237
|
+
const islandFile = path.join(paths.appDir, name, `${pc}.island.tsx`);
|
|
238
|
+
try {
|
|
239
|
+
await fs.mkdir(path.dirname(islandFile), { recursive: true });
|
|
240
|
+
await Bun.write(islandFile, generateIslandSource(pc, "visible"));
|
|
241
|
+
steps.push({ step: "create_island", success: true, result: { file: `app/${name}/${pc}.island.tsx` } });
|
|
242
|
+
} catch (e) {
|
|
243
|
+
steps.push({ step: "create_island", success: false, error: toErrorMessage(e) });
|
|
244
|
+
return { success: false, feature: name, steps, failedAt: "create_island" };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { success: true, feature: name, description, steps,
|
|
249
|
+
summary: { routesCreated: kinds.length, contractCreated: withContract, islandCreated: withIsland && kinds.includes("page") } };
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
"mandu.diagnose": async (args: Record<string, unknown>) => {
|
|
253
|
+
const { autoFix = false } = args as { autoFix?: boolean };
|
|
254
|
+
const [kitchenResult, guardResult, contractResult, manifestResult] = await Promise.all([
|
|
255
|
+
kitchen["mandu.kitchen.errors"]({ clear: false }).catch((e: Error) => ({ error: e.message })),
|
|
256
|
+
guard["mandu.guard.check"]({ autoCorrect: autoFix }).catch((e: Error) => ({ error: e.message })),
|
|
257
|
+
contract["mandu.contract.validate"]({}).catch((e: Error) => ({ error: e.message })),
|
|
258
|
+
spec["mandu.manifest.validate"]({}).catch((e: Error) => ({ error: e.message })),
|
|
259
|
+
]);
|
|
260
|
+
const checks = [
|
|
261
|
+
{ name: "kitchen_errors", result: kitchenResult },
|
|
262
|
+
{ name: "guard_check", result: guardResult },
|
|
263
|
+
{ name: "contract_validation", result: contractResult },
|
|
264
|
+
{ name: "manifest_validation", result: manifestResult },
|
|
265
|
+
];
|
|
266
|
+
const isFail = (c: typeof checks[number]) => {
|
|
267
|
+
const r = c.result as Record<string, unknown>;
|
|
268
|
+
return r.error || r.passed === false || r.valid === false;
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
healthy: !checks.some(isFail), autoFix, checks,
|
|
272
|
+
summary: { total: checks.length, passed: checks.filter((c) => !isFail(c)).length, failed: checks.filter(isFail).length },
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
"mandu.island.add": async (args: Record<string, unknown>) => {
|
|
277
|
+
const lockCheck = requireLock(args.lockId as string | undefined);
|
|
278
|
+
if (!lockCheck.allowed) {
|
|
279
|
+
return { error: lockCheck.error, hint: "Use mandu.tx.begin to acquire a lock first" };
|
|
280
|
+
}
|
|
281
|
+
const { name, route, strategy = "visible" } = args as {
|
|
282
|
+
name: string; route: string; strategy?: "load" | "idle" | "visible" | "media" | "never";
|
|
283
|
+
};
|
|
284
|
+
const islandFileName = `${name}.island.tsx`;
|
|
285
|
+
const islandRelPath = `app/${route}/${islandFileName}`;
|
|
286
|
+
const islandFullPath = path.join(paths.appDir, route, islandFileName);
|
|
287
|
+
|
|
288
|
+
try { await fs.access(islandFullPath); return { success: false, error: `Island file already exists: ${islandRelPath}` }; } catch { /* proceed */ }
|
|
289
|
+
|
|
290
|
+
await fs.mkdir(path.dirname(islandFullPath), { recursive: true });
|
|
291
|
+
await Bun.write(islandFullPath, generateIslandSource(name, strategy));
|
|
292
|
+
return {
|
|
293
|
+
success: true, file: islandRelPath, component: name, strategy,
|
|
294
|
+
nextSteps: [`Import <${name} /> in app/${route}/page.tsx`, "Run mandu_build to compile the client bundle", `Island hydrates on '${strategy}'`],
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
"mandu.middleware.add": async (args: Record<string, unknown>) => {
|
|
299
|
+
const { preset, options = {} } = args as { preset: "jwt" | "cors" | "auth" | "default"; options?: Record<string, string> };
|
|
300
|
+
const mwPath = path.join(paths.appDir, "middleware.ts");
|
|
301
|
+
try { await fs.access(mwPath); return { created: false, error: "middleware.ts already exists", path: "app/middleware.ts" }; } catch { /* proceed */ }
|
|
302
|
+
await fs.mkdir(path.dirname(mwPath), { recursive: true });
|
|
303
|
+
await Bun.write(mwPath, generateMiddlewareSource(preset, options));
|
|
304
|
+
return { created: true, path: "app/middleware.ts", preset };
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
"mandu.test.route": async (args: Record<string, unknown>) => {
|
|
308
|
+
const { routeId, quick = false } = args as { routeId: string; quick?: boolean };
|
|
309
|
+
const steps: { step: string; result: unknown }[] = [];
|
|
310
|
+
if (!quick) {
|
|
311
|
+
steps.push({ step: "extract", result: await ate["mandu.ate.extract"]({ repoRoot: projectRoot, routeGlobs: [`app/${routeId.replace(/-/g, "/")}/**`] }) });
|
|
312
|
+
}
|
|
313
|
+
steps.push({ step: "generate", result: await ate["mandu.ate.generate"]({ repoRoot: projectRoot, oracleLevel: "L1", onlyRoutes: [routeId] }) });
|
|
314
|
+
const runResult = await ate["mandu.ate.run"]({ repoRoot: projectRoot }) as { runId?: string; startedAt?: string; finishedAt?: string; exitCode?: number };
|
|
315
|
+
steps.push({ step: "run", result: runResult });
|
|
316
|
+
const report = await ate["mandu.ate.report"]({
|
|
317
|
+
repoRoot: projectRoot, runId: runResult.runId ?? "unknown",
|
|
318
|
+
startedAt: runResult.startedAt ?? new Date().toISOString(), finishedAt: runResult.finishedAt ?? new Date().toISOString(),
|
|
319
|
+
exitCode: runResult.exitCode ?? 1,
|
|
320
|
+
});
|
|
321
|
+
steps.push({ step: "report", result: report });
|
|
322
|
+
const passed = runResult.exitCode === 0;
|
|
323
|
+
let healSuggestions: unknown | undefined;
|
|
324
|
+
if (!passed && runResult.runId) {
|
|
325
|
+
healSuggestions = await ate["mandu.ate.heal"]({ repoRoot: projectRoot, runId: runResult.runId }).catch(() => undefined);
|
|
326
|
+
}
|
|
327
|
+
return { passed, routeId, results: steps, ...(healSuggestions ? { healSuggestions } : {}) };
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
"mandu.deploy.check": async (args: Record<string, unknown>) => {
|
|
331
|
+
const { target = "bun" } = args as { target?: "bun" | "docker" | "node" };
|
|
332
|
+
const [guardResult, contractResult, manifestResult] = await Promise.all([
|
|
333
|
+
guard["mandu.guard.check"]({ autoCorrect: false }).catch((e: Error) => ({ error: e.message })),
|
|
334
|
+
contract["mandu.contract.validate"]({}).catch((e: Error) => ({ error: e.message })),
|
|
335
|
+
spec["mandu.manifest.validate"]({}).catch((e: Error) => ({ error: e.message })),
|
|
336
|
+
]);
|
|
337
|
+
const status = (r: Record<string, unknown>): "pass" | "fail" => (r.error || r.passed === false || r.valid === false) ? "fail" : "pass";
|
|
338
|
+
const checks = { guard: status(guardResult as Record<string, unknown>), contracts: status(contractResult as Record<string, unknown>), manifest: status(manifestResult as Record<string, unknown>) };
|
|
339
|
+
const blockers: string[] = [];
|
|
340
|
+
const warnings: string[] = [];
|
|
341
|
+
if (checks.guard === "fail") blockers.push("Guard check failed — fix structural violations before deploying");
|
|
342
|
+
if (checks.contracts === "fail") blockers.push("Contract validation failed — fix schema mismatches");
|
|
343
|
+
if (checks.manifest === "fail") warnings.push("Manifest validation has issues — regenerate with mandu.generate");
|
|
344
|
+
if (target === "docker") warnings.push("Ensure Dockerfile copies .mandu/generated/ into the image");
|
|
345
|
+
if (target === "node") warnings.push("Node target requires Bun APIs to be polyfilled or avoided");
|
|
346
|
+
return { ready: blockers.length === 0, target, checks, blockers, warnings };
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
"mandu.cache.manage": async (args: Record<string, unknown>) => {
|
|
350
|
+
const { action, path: routePath, tag } = args as { action: "stats" | "clear"; path?: string; tag?: string };
|
|
351
|
+
const kitchenResult = await kitchen["mandu.kitchen.errors"]({ clear: false }).catch(() => null);
|
|
352
|
+
const kitchenInfo = isRecord(kitchenResult) ? kitchenResult : null;
|
|
353
|
+
const runtimeResult = await requestRuntimeCache(projectRoot, action, {
|
|
354
|
+
...(routePath ? { path: routePath } : {}),
|
|
355
|
+
...(tag ? { tag } : {}),
|
|
356
|
+
...(action === "clear" && !routePath && !tag ? { all: true } : {}),
|
|
357
|
+
}).catch(() => null);
|
|
358
|
+
const runtimeBody = isRecord(runtimeResult?.body) ? runtimeResult.body : null;
|
|
359
|
+
const serverStatus = runtimeResult?.response.ok ? "up" : kitchenInfo?.success === true ? "up" : "down";
|
|
360
|
+
|
|
361
|
+
if (action === "stats") {
|
|
362
|
+
return {
|
|
363
|
+
action: "stats",
|
|
364
|
+
serverStatus,
|
|
365
|
+
mode: runtimeResult?.control.mode ?? null,
|
|
366
|
+
stats: runtimeBody?.stats ?? null,
|
|
367
|
+
kitchen: kitchenInfo,
|
|
368
|
+
message: typeof runtimeBody?.message === "string"
|
|
369
|
+
? runtimeBody.message
|
|
370
|
+
: typeof kitchenInfo?.message === "string"
|
|
371
|
+
? kitchenInfo.message
|
|
372
|
+
: serverStatus === "up"
|
|
373
|
+
? "Runtime cache endpoint reachable — server is running."
|
|
374
|
+
: "Runtime cache endpoint unreachable — start `mandu dev` or `mandu start` first.",
|
|
375
|
+
hint: serverStatus === "up"
|
|
376
|
+
? "Use `mandu cache clear <path>` or `mandu cache clear --tag=<tag>` to invalidate runtime cache."
|
|
377
|
+
: "Start `mandu dev` or `mandu start` before requesting cache diagnostics.",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const target = routePath ? `path=${routePath}` : tag ? `tag=${tag}` : "all";
|
|
382
|
+
return {
|
|
383
|
+
action: "clear",
|
|
384
|
+
target,
|
|
385
|
+
serverStatus,
|
|
386
|
+
mode: runtimeResult?.control.mode ?? null,
|
|
387
|
+
cleared: typeof runtimeBody?.cleared === "number" ? runtimeBody.cleared : null,
|
|
388
|
+
stats: runtimeBody?.stats ?? null,
|
|
389
|
+
kitchen: kitchenInfo,
|
|
390
|
+
message: typeof runtimeBody?.error === "string"
|
|
391
|
+
? runtimeBody.error
|
|
392
|
+
: typeof runtimeBody?.message === "string"
|
|
393
|
+
? runtimeBody.message
|
|
394
|
+
: runtimeResult?.response.ok
|
|
395
|
+
? "Runtime cache cleared successfully."
|
|
396
|
+
: "Runtime cache endpoint is unavailable.",
|
|
397
|
+
hint: serverStatus === "up"
|
|
398
|
+
? "Clear by path or tag against the running local server."
|
|
399
|
+
: "Start the dev or production server first, then trigger revalidation or restart the process.",
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function toPascalCase(kebab: string): string {
|
|
406
|
+
return kebab.split(/[-_]/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function generateMiddlewareSource(preset: string, options: Record<string, string>): string {
|
|
410
|
+
const templates: Record<string, string> = {
|
|
411
|
+
jwt: `import type { MiddlewareHandler } from "@mandujs/core";\n\nexport const middleware: MiddlewareHandler = async (req, next) => {\n const token = req.headers.get("Authorization")?.replace("Bearer ", "");\n if (!token) return new Response("Unauthorized", { status: 401 });\n // TODO: verify JWT token with your secret\n return next(req);\n};\n`,
|
|
412
|
+
cors: `import type { MiddlewareHandler } from "@mandujs/core";\n\nconst ALLOWED_ORIGINS = ${JSON.stringify(options.origins?.split(",") ?? ["*"])};\n\nexport const middleware: MiddlewareHandler = async (req, next) => {\n const origin = req.headers.get("Origin") ?? "";\n const res = await next(req);\n if (ALLOWED_ORIGINS.includes("*") || ALLOWED_ORIGINS.includes(origin)) {\n res.headers.set("Access-Control-Allow-Origin", origin || "*");\n res.headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");\n res.headers.set("Access-Control-Allow-Headers", "Content-Type,Authorization");\n }\n return res;\n};\n`,
|
|
413
|
+
auth: `import type { MiddlewareHandler } from "@mandujs/core";\n\nconst PUBLIC_PATHS = ["/", "/login", "/api/auth"];\n\nexport const middleware: MiddlewareHandler = async (req, next) => {\n const url = new URL(req.url);\n if (PUBLIC_PATHS.some((p) => url.pathname.startsWith(p))) return next(req);\n const session = req.headers.get("Cookie")?.includes("session=");\n if (!session) return Response.redirect(new URL("/login", req.url));\n return next(req);\n};\n`,
|
|
414
|
+
default: `import type { MiddlewareHandler } from "@mandujs/core";\n\nexport const middleware: MiddlewareHandler = async (req, next) => {\n const start = Date.now();\n const res = await next(req);\n res.headers.set("X-Response-Time", \`\${Date.now() - start}ms\`);\n return res;\n};\n`,
|
|
415
|
+
};
|
|
416
|
+
return templates[preset] ?? templates.default;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function generateIslandSource(name: string, strategy: string): string {
|
|
420
|
+
return `"use client";
|
|
421
|
+
import { island } from "@mandujs/core/client";
|
|
422
|
+
import { useState } from "react";
|
|
423
|
+
|
|
424
|
+
interface ${name}Props {
|
|
425
|
+
[key: string]: unknown;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function ${name}Inner(props: ${name}Props) {
|
|
429
|
+
const [count, setCount] = useState(0);
|
|
430
|
+
return (
|
|
431
|
+
<div data-island="${name}">
|
|
432
|
+
<p>Island: ${name}</p>
|
|
433
|
+
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export default island("${strategy}", ${name}Inner);
|
|
439
|
+
`;
|
|
440
|
+
}
|