mcp-server-nuxt 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +243 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2151 -0
- package/package.json +58 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,2151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
// src/context.ts
|
|
9
|
+
import path5 from "path";
|
|
10
|
+
import { promises as fs5 } from "fs";
|
|
11
|
+
|
|
12
|
+
// src/utils/config-reader.ts
|
|
13
|
+
import { promises as fs, existsSync, statSync } from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var CONFIG_FILENAMES = [
|
|
16
|
+
"nuxt.config.ts",
|
|
17
|
+
"nuxt.config.js",
|
|
18
|
+
"nuxt.config.mts",
|
|
19
|
+
"nuxt.config.mjs",
|
|
20
|
+
"nuxt.config.cjs"
|
|
21
|
+
];
|
|
22
|
+
function detectProjectRoot(start = process.cwd()) {
|
|
23
|
+
let current = path.resolve(start);
|
|
24
|
+
try {
|
|
25
|
+
if (statSync(current).isFile()) return path.dirname(current);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
for (; ; ) {
|
|
29
|
+
if (hasConfigFile(current)) return current;
|
|
30
|
+
const parent = path.dirname(current);
|
|
31
|
+
if (parent === current) return path.resolve(start);
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function findConfigFile(root) {
|
|
36
|
+
for (const name of CONFIG_FILENAMES) {
|
|
37
|
+
const full = path.join(root, name);
|
|
38
|
+
if (existsSync(full)) return full;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function hasConfigFile(root) {
|
|
43
|
+
return findConfigFile(root) !== null;
|
|
44
|
+
}
|
|
45
|
+
async function loadNuxtConfig(projectRoot) {
|
|
46
|
+
const configPath = findConfigFile(projectRoot);
|
|
47
|
+
if (!configPath) {
|
|
48
|
+
return {
|
|
49
|
+
value: {},
|
|
50
|
+
source: null,
|
|
51
|
+
configPath: null,
|
|
52
|
+
sourceKind: "none",
|
|
53
|
+
error: `No nuxt.config file found in ${projectRoot}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let source = null;
|
|
57
|
+
try {
|
|
58
|
+
source = await fs.readFile(configPath, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const kit = await import("@nuxt/kit");
|
|
63
|
+
if (typeof kit.loadNuxtConfig === "function") {
|
|
64
|
+
const value = await kit.loadNuxtConfig({
|
|
65
|
+
rootDir: projectRoot,
|
|
66
|
+
overrides: { _mcpServerNuxt: true }
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
value: stripInternalKeys(value),
|
|
70
|
+
source,
|
|
71
|
+
configPath,
|
|
72
|
+
sourceKind: "resolved"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const value = parseConfigSource(source ?? "");
|
|
79
|
+
return {
|
|
80
|
+
value,
|
|
81
|
+
source,
|
|
82
|
+
configPath,
|
|
83
|
+
sourceKind: value && Object.keys(value).length > 0 ? "parsed" : "raw"
|
|
84
|
+
};
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return {
|
|
87
|
+
value: {},
|
|
88
|
+
source,
|
|
89
|
+
configPath,
|
|
90
|
+
sourceKind: "raw",
|
|
91
|
+
error: err instanceof Error ? err.message : String(err)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function stripInternalKeys(value) {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [key, v] of Object.entries(value)) {
|
|
98
|
+
if (key.startsWith("_") || key === "$schema") continue;
|
|
99
|
+
out[key] = v;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function parseConfigSource(source) {
|
|
104
|
+
const result = {};
|
|
105
|
+
const body = extractConfigBody(source);
|
|
106
|
+
if (!body) return result;
|
|
107
|
+
const assignments = splitTopLevel(body);
|
|
108
|
+
for (const assignment of assignments) {
|
|
109
|
+
const match = assignment.match(/^[\s,;]*["']?([\w$.-]+)["']?\s*[:=]\s*([\s\S]*)$/);
|
|
110
|
+
if (!match) continue;
|
|
111
|
+
const key = match[1];
|
|
112
|
+
const rawValue = match[2].trim().replace(/[,;]\s*$/, "");
|
|
113
|
+
result[key] = tryParseValue(rawValue);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
function extractConfigBody(source) {
|
|
118
|
+
const defMatch = source.match(/defineNuxtConfig\s*\(\s*(\{)/);
|
|
119
|
+
let startIndex = defMatch ? source.indexOf("{", defMatch.index ?? 0) : -1;
|
|
120
|
+
if (startIndex === -1) {
|
|
121
|
+
const expMatch = source.match(/export\s+default\s+(\{)/);
|
|
122
|
+
startIndex = expMatch ? source.indexOf("{", expMatch.index ?? 0) : -1;
|
|
123
|
+
}
|
|
124
|
+
if (startIndex === -1) return null;
|
|
125
|
+
let depth = 0;
|
|
126
|
+
let inString = null;
|
|
127
|
+
let inTemplate = false;
|
|
128
|
+
let inComment = null;
|
|
129
|
+
for (let i = startIndex; i < source.length; i++) {
|
|
130
|
+
const ch = source[i];
|
|
131
|
+
const prev = i > 0 ? source[i - 1] : "";
|
|
132
|
+
if (inComment === "line") {
|
|
133
|
+
if (ch === "\n") inComment = null;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (inComment === "block") {
|
|
137
|
+
if (ch === "*" && source[i + 1] === "/") {
|
|
138
|
+
inComment = null;
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (inString) {
|
|
144
|
+
if (ch === "\\") {
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === inString) inString = null;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (inTemplate) {
|
|
152
|
+
if (ch === "\\") {
|
|
153
|
+
i++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (ch === "`") inTemplate = false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
160
|
+
inComment = "line";
|
|
161
|
+
i++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
165
|
+
inComment = "block";
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch === '"' || ch === "'") {
|
|
170
|
+
inString = ch;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === "`") {
|
|
174
|
+
inTemplate = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === "{") depth++;
|
|
178
|
+
else if (ch === "}") {
|
|
179
|
+
depth--;
|
|
180
|
+
if (depth === 0) return source.slice(startIndex + 1, i);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function splitTopLevel(body) {
|
|
186
|
+
const parts = [];
|
|
187
|
+
let depth = 0;
|
|
188
|
+
let current = "";
|
|
189
|
+
let inString = null;
|
|
190
|
+
let inTemplate = false;
|
|
191
|
+
for (let i = 0; i < body.length; i++) {
|
|
192
|
+
const ch = body[i];
|
|
193
|
+
if (inString) {
|
|
194
|
+
current += ch;
|
|
195
|
+
if (ch === "\\") {
|
|
196
|
+
current += body[i + 1] ?? "";
|
|
197
|
+
i++;
|
|
198
|
+
} else if (ch === inString) inString = null;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (inTemplate) {
|
|
202
|
+
current += ch;
|
|
203
|
+
if (ch === "\\") {
|
|
204
|
+
current += body[i + 1] ?? "";
|
|
205
|
+
i++;
|
|
206
|
+
} else if (ch === "`") inTemplate = false;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (ch === '"' || ch === "'") {
|
|
210
|
+
inString = ch;
|
|
211
|
+
current += ch;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ch === "`") {
|
|
215
|
+
inTemplate = true;
|
|
216
|
+
current += ch;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (ch === "{" || ch === "[" || ch === "(") depth++;
|
|
220
|
+
else if (ch === "}" || ch === "]" || ch === ")") depth--;
|
|
221
|
+
if (ch === "," && depth === 0) {
|
|
222
|
+
parts.push(current);
|
|
223
|
+
current = "";
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
current += ch;
|
|
227
|
+
}
|
|
228
|
+
if (current.trim()) parts.push(current);
|
|
229
|
+
return parts;
|
|
230
|
+
}
|
|
231
|
+
function tryParseValue(raw) {
|
|
232
|
+
const trimmed = raw.trim();
|
|
233
|
+
if (trimmed === "") return void 0;
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(trimmed);
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
const strMatch = trimmed.match(/^(["'])([\s\S]*)\1$/);
|
|
239
|
+
if (strMatch) return strMatch[2];
|
|
240
|
+
const arrMatch = trimmed.match(/^\[([\s\S]*)\]$/);
|
|
241
|
+
if (arrMatch && !trimmed.includes("=>")) {
|
|
242
|
+
const items = splitTopLevel(arrMatch[1]).map((i) => i.trim()).filter(Boolean).map((i) => tryParseValue(i.replace(/^,/, "").trim()));
|
|
243
|
+
return items;
|
|
244
|
+
}
|
|
245
|
+
return { __raw: trimmed };
|
|
246
|
+
}
|
|
247
|
+
function getConfigValue(config, key) {
|
|
248
|
+
if (!key || key === "") return config;
|
|
249
|
+
const parts = key.split(".");
|
|
250
|
+
let current = config;
|
|
251
|
+
for (const part of parts) {
|
|
252
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
253
|
+
current = current[part];
|
|
254
|
+
}
|
|
255
|
+
return current;
|
|
256
|
+
}
|
|
257
|
+
function serializeValue(value, indent = 0) {
|
|
258
|
+
if (value === void 0) return "undefined";
|
|
259
|
+
if (value === null) return "null";
|
|
260
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
261
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
262
|
+
if (typeof value === "function") return String(value);
|
|
263
|
+
if (Array.isArray(value)) {
|
|
264
|
+
if (value.length === 0) return "[]";
|
|
265
|
+
const pad = " ".repeat(indent + 2);
|
|
266
|
+
const inner = value.map((v) => pad + serializeValue(v, indent + 2)).join(",\n");
|
|
267
|
+
return `[
|
|
268
|
+
${inner},
|
|
269
|
+
${" ".repeat(indent)}]`;
|
|
270
|
+
}
|
|
271
|
+
if (typeof value === "object") {
|
|
272
|
+
const entries = Object.entries(value);
|
|
273
|
+
if (entries.length === 0) return "{}";
|
|
274
|
+
const pad = " ".repeat(indent + 2);
|
|
275
|
+
const inner = entries.map(([k, v]) => `${pad}${JSON.stringify(k)}: ${serializeValue(v, indent + 2)}`).join(",\n");
|
|
276
|
+
return `{
|
|
277
|
+
${inner},
|
|
278
|
+
${" ".repeat(indent)}}`;
|
|
279
|
+
}
|
|
280
|
+
return String(value);
|
|
281
|
+
}
|
|
282
|
+
function setConfigValue(source, key, value, rawValue) {
|
|
283
|
+
const body = extractConfigBody(source);
|
|
284
|
+
if (!body) throw new Error("Could not locate defineNuxtConfig({...}) in nuxt.config");
|
|
285
|
+
const expression = rawValue ?? serializeValue(value);
|
|
286
|
+
const parts = key.split(".");
|
|
287
|
+
const topKey = parts[0];
|
|
288
|
+
const existing = findTopLevelAssignment(body, topKey);
|
|
289
|
+
let newBody;
|
|
290
|
+
if (!existing) {
|
|
291
|
+
newBody = insertBeforeClosing(body, `${topKey}: ${expression},`);
|
|
292
|
+
} else if (parts.length === 1) {
|
|
293
|
+
newBody = body.replace(existing.fullMatch, `${topKey}: ${expression}`);
|
|
294
|
+
} else {
|
|
295
|
+
const nested = setNestedKey(existing.value, parts.slice(1), value, rawValue);
|
|
296
|
+
newBody = body.replace(existing.fullMatch, `${topKey}: ${nested}`);
|
|
297
|
+
}
|
|
298
|
+
const newSource = replaceConfigBody(source, body, newBody);
|
|
299
|
+
const diff = createDiff(source, newSource);
|
|
300
|
+
return { newSource, diff };
|
|
301
|
+
}
|
|
302
|
+
function appendModule(source, name, options) {
|
|
303
|
+
const body = extractConfigBody(source);
|
|
304
|
+
if (!body) throw new Error("Could not locate defineNuxtConfig({...}) in nuxt.config");
|
|
305
|
+
const entry = options === void 0 ? JSON.stringify(name) : `[${JSON.stringify(name)}, ${serializeValue(options)}]`;
|
|
306
|
+
const existing = findTopLevelAssignment(body, "modules");
|
|
307
|
+
let newBody;
|
|
308
|
+
if (existing) {
|
|
309
|
+
const trimmed = existing.value.trim();
|
|
310
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
311
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
312
|
+
newBody = body.replace(existing.fullMatch, `modules: [${inner ? `${inner}, ${entry}` : entry}]`);
|
|
313
|
+
} else {
|
|
314
|
+
newBody = body.replace(existing.fullMatch, `modules: [${entry}]`);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
newBody = insertBeforeClosing(body, `modules: [${entry}],`);
|
|
318
|
+
}
|
|
319
|
+
const newSource = replaceConfigBody(source, body, newBody);
|
|
320
|
+
const diff = createDiff(source, newSource);
|
|
321
|
+
return { newSource, diff };
|
|
322
|
+
}
|
|
323
|
+
function findTopLevelAssignment(body, key) {
|
|
324
|
+
const assignments = splitTopLevel(body);
|
|
325
|
+
for (const assignment of assignments) {
|
|
326
|
+
const match = assignment.match(/^([\s]*)(["']?)([\w$.-]+)\2\s*:\s*([\s\S]*)$/);
|
|
327
|
+
if (!match) continue;
|
|
328
|
+
if (match[3] === key) {
|
|
329
|
+
return { fullMatch: assignment.trim(), value: match[4].trim() };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function insertBeforeClosing(body, line) {
|
|
335
|
+
const indent = detectIndent(body);
|
|
336
|
+
const trimmed = body.replace(/\s+$/, "");
|
|
337
|
+
return `${trimmed}
|
|
338
|
+
${indent}${line}
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
function detectIndent(body) {
|
|
342
|
+
const lines = body.split("\n");
|
|
343
|
+
const lastNonEmpty = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
344
|
+
return lastNonEmpty.match(/^\s*/)?.[0] ?? "";
|
|
345
|
+
}
|
|
346
|
+
function setNestedKey(valueText, parts, value, rawValue) {
|
|
347
|
+
const expression = rawValue ?? serializeValue(value);
|
|
348
|
+
const objMatch = valueText.match(/^\{([\s\S]*)\}$/);
|
|
349
|
+
if (!objMatch) {
|
|
350
|
+
return `{ ${parts[0]}: ${expression} }`;
|
|
351
|
+
}
|
|
352
|
+
const inner = objMatch[1];
|
|
353
|
+
const existing = findTopLevelAssignment(inner, parts[0]);
|
|
354
|
+
if (!existing) {
|
|
355
|
+
return `{${inner}
|
|
356
|
+
${parts[0]}: ${expression},
|
|
357
|
+
}`;
|
|
358
|
+
}
|
|
359
|
+
if (parts.length === 1) {
|
|
360
|
+
return `{${inner.replace(existing.fullMatch, `${parts[0]}: ${expression}`)}}`;
|
|
361
|
+
}
|
|
362
|
+
return `{${inner.replace(
|
|
363
|
+
existing.fullMatch,
|
|
364
|
+
`${parts[0]}: ${setNestedKey(existing.value, parts.slice(1), value, rawValue)}`
|
|
365
|
+
)}}`;
|
|
366
|
+
}
|
|
367
|
+
function replaceConfigBody(source, oldBody, newBody) {
|
|
368
|
+
const idx = source.indexOf(oldBody);
|
|
369
|
+
if (idx === -1) return source;
|
|
370
|
+
return source.slice(0, idx) + newBody + source.slice(idx + oldBody.length);
|
|
371
|
+
}
|
|
372
|
+
function createDiff(before, after) {
|
|
373
|
+
const beforeLines = before.split("\n");
|
|
374
|
+
const afterLines = after.split("\n");
|
|
375
|
+
const lines = [];
|
|
376
|
+
const maxLen = Math.max(beforeLines.length, afterLines.length);
|
|
377
|
+
let context = [];
|
|
378
|
+
for (let i = 0; i < maxLen; i++) {
|
|
379
|
+
const b = beforeLines[i];
|
|
380
|
+
const a = afterLines[i];
|
|
381
|
+
if (b === a) {
|
|
382
|
+
if (context.length) {
|
|
383
|
+
lines.push(...context.map((c) => ` ${c.text}`));
|
|
384
|
+
context = [];
|
|
385
|
+
}
|
|
386
|
+
lines.push(` ${b ?? ""}`);
|
|
387
|
+
} else {
|
|
388
|
+
if (context.length === 0) lines.push("---");
|
|
389
|
+
if (b !== void 0) context.push({ b: i, a: -1, text: `- ${b}` });
|
|
390
|
+
if (a !== void 0) context.push({ b: -1, a: i, text: `+ ${a}` });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (context.length) lines.push(...context.map((c) => ` ${c.text}`));
|
|
394
|
+
return lines.join("\n") || "(no changes)";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/utils/nuxt-parser.ts
|
|
398
|
+
import { promises as fs2 } from "fs";
|
|
399
|
+
import path2 from "path";
|
|
400
|
+
import fg from "fast-glob";
|
|
401
|
+
var DEFAULT_IGNORE = ["**/node_modules/**", "**/.nuxt/**", "**/.output/**", "**/dist/**", "**/.git/**"];
|
|
402
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
403
|
+
".vue",
|
|
404
|
+
".ts",
|
|
405
|
+
".js",
|
|
406
|
+
".mts",
|
|
407
|
+
".mjs",
|
|
408
|
+
".cts",
|
|
409
|
+
".cjs",
|
|
410
|
+
".tsx",
|
|
411
|
+
".jsx",
|
|
412
|
+
".json",
|
|
413
|
+
".md",
|
|
414
|
+
".mdx",
|
|
415
|
+
".css",
|
|
416
|
+
".scss",
|
|
417
|
+
".html",
|
|
418
|
+
".yml",
|
|
419
|
+
".yaml"
|
|
420
|
+
]);
|
|
421
|
+
async function findFiles(root, patterns, extraIgnore = []) {
|
|
422
|
+
const files = await fg(patterns, {
|
|
423
|
+
cwd: root,
|
|
424
|
+
absolute: true,
|
|
425
|
+
onlyFiles: true,
|
|
426
|
+
dot: false,
|
|
427
|
+
ignore: [...DEFAULT_IGNORE, ...extraIgnore]
|
|
428
|
+
});
|
|
429
|
+
return files.sort();
|
|
430
|
+
}
|
|
431
|
+
function relativePath(root, file) {
|
|
432
|
+
const rel = path2.relative(root, file);
|
|
433
|
+
return rel.split(path2.sep).join("/");
|
|
434
|
+
}
|
|
435
|
+
async function readIfExists(file) {
|
|
436
|
+
try {
|
|
437
|
+
return await fs2.readFile(file, "utf8");
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function exists(file) {
|
|
443
|
+
try {
|
|
444
|
+
await fs2.access(file);
|
|
445
|
+
return true;
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function pascalCase(segment) {
|
|
451
|
+
return segment.replace(/\.(vue|ts|js|mts|mjs|tsx|jsx)$/, "").split(/[-_.\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
452
|
+
}
|
|
453
|
+
function camelCase(segment) {
|
|
454
|
+
return segment.replace(/\.(ts|js|mts|mjs)$/, "").split(/[-_.\s]+/).filter(Boolean).map((word, i) => i === 0 ? word.charAt(0).toLowerCase() + word.slice(1) : word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
455
|
+
}
|
|
456
|
+
function componentAutoImportName(relPath) {
|
|
457
|
+
return relPath.split("/").map(pascalCase).join("");
|
|
458
|
+
}
|
|
459
|
+
function convertDynamicSegment(segment) {
|
|
460
|
+
const optional = segment.match(/^\[\[(.+)\]\]$/);
|
|
461
|
+
if (optional) {
|
|
462
|
+
return { path: `:${optional[1]}?`, param: { name: optional[1], optional: true, catchAll: false } };
|
|
463
|
+
}
|
|
464
|
+
const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
|
|
465
|
+
if (catchAll) {
|
|
466
|
+
return { path: `:${catchAll[1]}(.*)*`, param: { name: catchAll[1], optional: false, catchAll: true } };
|
|
467
|
+
}
|
|
468
|
+
const dynamic = segment.match(/^\[(.+)\]$/);
|
|
469
|
+
if (dynamic) {
|
|
470
|
+
return { path: `:${dynamic[1]}`, param: { name: dynamic[1], optional: false, catchAll: false } };
|
|
471
|
+
}
|
|
472
|
+
return { path: segment };
|
|
473
|
+
}
|
|
474
|
+
function fileToRoutePath(relPath) {
|
|
475
|
+
const withoutExt = relPath.replace(/\.vue$/, "");
|
|
476
|
+
const segments = withoutExt.split("/").filter((s) => s !== "");
|
|
477
|
+
const outSegments = [];
|
|
478
|
+
const params = [];
|
|
479
|
+
for (const segment of segments) {
|
|
480
|
+
if (segment === "index") continue;
|
|
481
|
+
const { path: converted, param } = convertDynamicSegment(segment);
|
|
482
|
+
outSegments.push(converted);
|
|
483
|
+
if (param) params.push(param);
|
|
484
|
+
}
|
|
485
|
+
return { path: "/" + outSegments.join("/"), params };
|
|
486
|
+
}
|
|
487
|
+
function routeSegmentToFile(segment) {
|
|
488
|
+
const optional = segment.match(/^:(.+)\?$/);
|
|
489
|
+
if (optional) return `[[${optional[1]}]]`;
|
|
490
|
+
const catchAll = segment.match(/^:(.+)\(\.\*\)\*$/);
|
|
491
|
+
if (catchAll) return `[...${catchAll[1]}]`;
|
|
492
|
+
const dynamic = segment.match(/^:(.+)$/);
|
|
493
|
+
if (dynamic) return `[${dynamic[1]}]`;
|
|
494
|
+
return segment;
|
|
495
|
+
}
|
|
496
|
+
function routePathToFile(routePath) {
|
|
497
|
+
const trimmed = routePath.replace(/^\/+|\/+$/g, "");
|
|
498
|
+
if (!trimmed) return "pages/index.vue";
|
|
499
|
+
const segments = trimmed.split("/").map(routeSegmentToFile);
|
|
500
|
+
return "pages/" + segments.join("/") + ".vue";
|
|
501
|
+
}
|
|
502
|
+
function routePathToNitroFile(apiPath, method) {
|
|
503
|
+
let trimmed = apiPath.replace(/^\/+|\/+$/g, "");
|
|
504
|
+
if (trimmed === "api") trimmed = "";
|
|
505
|
+
else if (trimmed.startsWith("api/")) trimmed = trimmed.slice("api/".length);
|
|
506
|
+
const segments = trimmed ? trimmed.split("/").map(routeSegmentToFile) : [];
|
|
507
|
+
const base = segments.length ? segments.join("/") : "index";
|
|
508
|
+
const suffix = method && method !== "*" ? `.${method.toLowerCase()}` : "";
|
|
509
|
+
return `server/api/${base}${suffix}.ts`;
|
|
510
|
+
}
|
|
511
|
+
function extractPageMeta(content) {
|
|
512
|
+
const meta = {};
|
|
513
|
+
let middleware = [];
|
|
514
|
+
let layout;
|
|
515
|
+
const defMatch = content.match(/definePageMeta\s*\(\s*\{([\s\S]*?)\}\s*\)/);
|
|
516
|
+
if (!defMatch) return { meta, middleware, layout };
|
|
517
|
+
const body = defMatch[1];
|
|
518
|
+
for (const line of body.split("\n")) {
|
|
519
|
+
const layoutMatch = line.match(/layout\s*:\s*["']([^"']+)["']/);
|
|
520
|
+
if (layoutMatch) layout = layoutMatch[1];
|
|
521
|
+
const mwMatch = line.match(/middleware\s*:\s*(\[[\s\S]*?\]|["'][^"']+["'])/);
|
|
522
|
+
if (mwMatch) {
|
|
523
|
+
const raw = mwMatch[1];
|
|
524
|
+
if (raw.startsWith("[")) {
|
|
525
|
+
middleware = [...raw.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
|
|
526
|
+
} else {
|
|
527
|
+
middleware = [raw.replace(/["']/g, "")];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const kv = line.match(/^\s*([\w$]+)\s*:\s*([\s\S]*?)\s*(?:,|$)/);
|
|
531
|
+
if (kv && !["layout", "middleware"].includes(kv[1])) {
|
|
532
|
+
meta[kv[1]] = kv[2].trim();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { meta, middleware, layout };
|
|
536
|
+
}
|
|
537
|
+
async function listRoutesIn(scanRoot, layer) {
|
|
538
|
+
const files = await findFiles(scanRoot, ["pages/**/*.vue"]);
|
|
539
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
540
|
+
for (const file of files) {
|
|
541
|
+
const rel = relativePath(scanRoot, file).replace(/^pages\//, "");
|
|
542
|
+
byPath.set(fileToRoutePath(rel).path, file);
|
|
543
|
+
}
|
|
544
|
+
const routes = [];
|
|
545
|
+
for (const file of files) {
|
|
546
|
+
const rel = relativePath(scanRoot, file).replace(/^pages\//, "");
|
|
547
|
+
const { path: routePath, params } = fileToRoutePath(rel);
|
|
548
|
+
const content = await readIfExists(file) ?? "";
|
|
549
|
+
const { meta, middleware, layout } = extractPageMeta(content);
|
|
550
|
+
const children = [...byPath.keys()].filter(
|
|
551
|
+
(p) => p.startsWith(routePath === "/" ? "/" : routePath + "/") && p !== routePath
|
|
552
|
+
);
|
|
553
|
+
routes.push({
|
|
554
|
+
path: routePath,
|
|
555
|
+
file: relativePath(scanRoot, file),
|
|
556
|
+
absolutePath: file,
|
|
557
|
+
params,
|
|
558
|
+
meta,
|
|
559
|
+
middleware,
|
|
560
|
+
layout,
|
|
561
|
+
nested: children.length > 0,
|
|
562
|
+
children,
|
|
563
|
+
layer
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return routes;
|
|
567
|
+
}
|
|
568
|
+
async function listPageRoutes(root, layers = []) {
|
|
569
|
+
const targets = [{ cwd: root, name: void 0 }, ...layers.map((l) => ({ cwd: l.cwd, name: l.name }))];
|
|
570
|
+
const routes = [];
|
|
571
|
+
const seen = /* @__PURE__ */ new Set();
|
|
572
|
+
for (const target of targets) {
|
|
573
|
+
for (const route of await listRoutesIn(target.cwd, target.name)) {
|
|
574
|
+
if (seen.has(route.absolutePath)) continue;
|
|
575
|
+
seen.add(route.absolutePath);
|
|
576
|
+
routes.push(route);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
580
|
+
}
|
|
581
|
+
function parseProps(content) {
|
|
582
|
+
const props = [];
|
|
583
|
+
const typeMatch = content.match(/defineProps\s*<\s*\{([\s\S]*?)\}\s*>/);
|
|
584
|
+
if (typeMatch) {
|
|
585
|
+
const body = typeMatch[1];
|
|
586
|
+
for (const entry of splitTopLevelComma(body)) {
|
|
587
|
+
const m = entry.trim().match(/^([\w$]+)(\??)\s*:\s*([\s\S]*)$/);
|
|
588
|
+
if (!m) continue;
|
|
589
|
+
props.push({
|
|
590
|
+
name: m[1],
|
|
591
|
+
type: m[3].trim(),
|
|
592
|
+
required: m[2] !== "?"
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return props;
|
|
596
|
+
}
|
|
597
|
+
const runtimeMatch = content.match(/defineProps\s*\(\s*\{([\s\S]*?)\}\s*\)/);
|
|
598
|
+
if (runtimeMatch) {
|
|
599
|
+
const body = runtimeMatch[1];
|
|
600
|
+
for (const entry of splitTopLevelComma(body)) {
|
|
601
|
+
const m = entry.trim().match(/^([\w$]+)\s*:\s*([\s\S]*)$/);
|
|
602
|
+
if (!m) continue;
|
|
603
|
+
const name = m[1];
|
|
604
|
+
const value = m[2].trim();
|
|
605
|
+
if (value.startsWith("{")) {
|
|
606
|
+
const typeM = value.match(/\btype\s*:\s*([\w$[\].]+)/);
|
|
607
|
+
const reqM = value.match(/\brequired\s*:\s*(true|false)/);
|
|
608
|
+
const defM = value.match(/\bdefault\s*:\s*([^,}]+)/);
|
|
609
|
+
props.push({
|
|
610
|
+
name,
|
|
611
|
+
type: typeM?.[1],
|
|
612
|
+
required: reqM?.[1] === "true",
|
|
613
|
+
default: defM?.[1]?.trim()
|
|
614
|
+
});
|
|
615
|
+
} else {
|
|
616
|
+
props.push({ name, type: value.replace(/[()]/g, "").trim(), required: false });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return props;
|
|
621
|
+
}
|
|
622
|
+
function parseEmits(content) {
|
|
623
|
+
const emits = [];
|
|
624
|
+
const runtimeMatch = content.match(/defineEmits\s*\(\s*\[([\s\S]*?)\]\s*\)/);
|
|
625
|
+
if (runtimeMatch) {
|
|
626
|
+
for (const m of runtimeMatch[1].matchAll(/["']([^"']+)["']/g)) emits.push(m[1]);
|
|
627
|
+
return emits;
|
|
628
|
+
}
|
|
629
|
+
const typeMatch = content.match(/defineEmits\s*<\s*\{([\s\S]*?)\}\s*>/);
|
|
630
|
+
if (typeMatch) {
|
|
631
|
+
for (const m of typeMatch[1].matchAll(/['"]?([\w:$-]+)['"]?\s*\(/g)) {
|
|
632
|
+
if (!["on", "call"].includes(m[1])) emits.push(m[1]);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return emits;
|
|
636
|
+
}
|
|
637
|
+
function parseSlots(content) {
|
|
638
|
+
const slots = /* @__PURE__ */ new Set();
|
|
639
|
+
for (const m of content.matchAll(/<slot(?:\s+name=["']([^"']+)["'])?[^>]*\/?>/g)) {
|
|
640
|
+
slots.add(m[1] ?? "default");
|
|
641
|
+
}
|
|
642
|
+
if (/\buseSlots\s*\(/.test(content)) slots.add("default");
|
|
643
|
+
return [...slots];
|
|
644
|
+
}
|
|
645
|
+
function splitTopLevelComma(text) {
|
|
646
|
+
const parts = [];
|
|
647
|
+
let depth = 0;
|
|
648
|
+
let current = "";
|
|
649
|
+
for (const ch of text) {
|
|
650
|
+
if (ch === "{" || ch === "[" || ch === "(") depth++;
|
|
651
|
+
else if (ch === "}" || ch === "]" || ch === ")") depth--;
|
|
652
|
+
if (ch === "," && depth === 0) {
|
|
653
|
+
parts.push(current);
|
|
654
|
+
current = "";
|
|
655
|
+
} else {
|
|
656
|
+
current += ch;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (current.trim()) parts.push(current);
|
|
660
|
+
return parts;
|
|
661
|
+
}
|
|
662
|
+
async function parseVueComponent(file) {
|
|
663
|
+
const content = await readIfExists(file) ?? "";
|
|
664
|
+
const scriptSetup = content.match(/<script\s+setup[^>]*>([\s\S]*?)<\/script>/)?.[1] ?? "";
|
|
665
|
+
const template = content.match(/<template>([\s\S]*?)<\/template>/)?.[1] ?? content;
|
|
666
|
+
const combined = scriptSetup + "\n" + template;
|
|
667
|
+
return {
|
|
668
|
+
props: parseProps(scriptSetup || combined),
|
|
669
|
+
emits: parseEmits(scriptSetup),
|
|
670
|
+
slots: parseSlots(template),
|
|
671
|
+
hasScriptSetup: scriptSetup.length > 0
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async function listComponents(root, layers = []) {
|
|
675
|
+
const targets = [{ cwd: root, name: void 0 }, ...layers.map((l) => ({ cwd: l.cwd, name: l.name }))];
|
|
676
|
+
const components = [];
|
|
677
|
+
const seen = /* @__PURE__ */ new Set();
|
|
678
|
+
for (const target of targets) {
|
|
679
|
+
const files = await findFiles(target.cwd, ["components/**/*.vue"]);
|
|
680
|
+
for (const file of files) {
|
|
681
|
+
if (seen.has(file)) continue;
|
|
682
|
+
seen.add(file);
|
|
683
|
+
const rel = relativePath(target.cwd, file);
|
|
684
|
+
const relFromComponents = rel.replace(/^components\//, "");
|
|
685
|
+
const { props, emits, slots } = await parseVueComponent(file);
|
|
686
|
+
components.push({
|
|
687
|
+
name: componentAutoImportName(relFromComponents),
|
|
688
|
+
file: rel,
|
|
689
|
+
absolutePath: file,
|
|
690
|
+
directory: "components",
|
|
691
|
+
props,
|
|
692
|
+
emits,
|
|
693
|
+
slots,
|
|
694
|
+
autoImported: true,
|
|
695
|
+
isPage: false,
|
|
696
|
+
layer: target.name
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return components.sort((a, b) => a.name.localeCompare(b.name));
|
|
701
|
+
}
|
|
702
|
+
async function parseComposable(file) {
|
|
703
|
+
const content = await readIfExists(file) ?? "";
|
|
704
|
+
const fnMatch = content.match(/export\s+(?:async\s+)?function\s+([\w$]+)\s*\(([\s\S]*?)\)\s*(?::\s*([^{=]+))?\s*\{/);
|
|
705
|
+
if (!fnMatch) return {};
|
|
706
|
+
const params = fnMatch[2].trim();
|
|
707
|
+
const returnType = fnMatch[3]?.trim() ?? void 0;
|
|
708
|
+
return { parameters: params || void 0, returnType };
|
|
709
|
+
}
|
|
710
|
+
async function listComposables(root, layers = []) {
|
|
711
|
+
const targets = [{ cwd: root, name: void 0 }, ...layers.map((l) => ({ cwd: l.cwd, name: l.name }))];
|
|
712
|
+
const composables = [];
|
|
713
|
+
const seen = /* @__PURE__ */ new Set();
|
|
714
|
+
for (const target of targets) {
|
|
715
|
+
const files = await findFiles(target.cwd, ["composables/**/*.{ts,js,mts,mjs}", "utils/**/*.{ts,js,mts,mjs}"]);
|
|
716
|
+
for (const file of files) {
|
|
717
|
+
if (seen.has(file)) continue;
|
|
718
|
+
seen.add(file);
|
|
719
|
+
const rel = relativePath(target.cwd, file);
|
|
720
|
+
const dir = rel.startsWith("composables/") ? "composables" : "utils";
|
|
721
|
+
const basename = rel.split("/").pop() ?? "";
|
|
722
|
+
const name = camelCase(basename);
|
|
723
|
+
const { parameters, returnType } = await parseComposable(file);
|
|
724
|
+
const exported = await readIfExists(file) ?? "";
|
|
725
|
+
const isAutoImported = /^use[A-Z]/.test(name) || /export\s+(?:default|const)\s+/.test(exported) || /\bautoImport\s*:\s*true/.test(exported);
|
|
726
|
+
composables.push({
|
|
727
|
+
name,
|
|
728
|
+
file: rel,
|
|
729
|
+
absolutePath: file,
|
|
730
|
+
parameters,
|
|
731
|
+
returnType,
|
|
732
|
+
autoImported: isAutoImported,
|
|
733
|
+
directory: dir,
|
|
734
|
+
layer: target.name
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return composables.sort((a, b) => a.name.localeCompare(b.name));
|
|
739
|
+
}
|
|
740
|
+
var METHOD_SUFFIXES = {
|
|
741
|
+
get: "GET",
|
|
742
|
+
post: "POST",
|
|
743
|
+
put: "PUT",
|
|
744
|
+
patch: "PATCH",
|
|
745
|
+
delete: "DELETE",
|
|
746
|
+
head: "HEAD",
|
|
747
|
+
options: "OPTIONS"
|
|
748
|
+
};
|
|
749
|
+
function nitroPathFromFile(rel) {
|
|
750
|
+
const withoutExt = rel.replace(/\.(ts|js|mts|mjs)$/, "");
|
|
751
|
+
const segments = withoutExt.split("/");
|
|
752
|
+
const last = segments[segments.length - 1];
|
|
753
|
+
let method = "*";
|
|
754
|
+
let lastSegment = last;
|
|
755
|
+
for (const [suffix, httpMethod] of Object.entries(METHOD_SUFFIXES)) {
|
|
756
|
+
if (last.endsWith(`.${suffix}`)) {
|
|
757
|
+
method = httpMethod;
|
|
758
|
+
lastSegment = last.slice(0, -(suffix.length + 1));
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
segments[segments.length - 1] = lastSegment;
|
|
763
|
+
const converted = segments.map((s) => convertDynamicSegment(s).path);
|
|
764
|
+
if (converted[converted.length - 1] === "index") converted.pop();
|
|
765
|
+
return { path: "/" + converted.join("/"), method };
|
|
766
|
+
}
|
|
767
|
+
async function listServerRoutes(root) {
|
|
768
|
+
const files = await findFiles(root, [
|
|
769
|
+
"server/api/**/*.{ts,js,mts,mjs}",
|
|
770
|
+
"server/routes/**/*.{ts,js,mts,mjs}",
|
|
771
|
+
"server/middleware/**/*.{ts,js,mts,mjs}",
|
|
772
|
+
"server/plugins/**/*.{ts,js,mts,mjs}"
|
|
773
|
+
]);
|
|
774
|
+
const globalMiddleware = files.filter((f) => relativePath(root, f).startsWith("server/middleware/")).map((f) => relativePath(root, f).replace(/^server\/middleware\//, "").replace(/\.(ts|js|mts|mjs)$/, ""));
|
|
775
|
+
const routes = [];
|
|
776
|
+
for (const file of files) {
|
|
777
|
+
const rel = relativePath(root, file);
|
|
778
|
+
let kind;
|
|
779
|
+
let pathPrefix = "";
|
|
780
|
+
if (rel.startsWith("server/api/")) {
|
|
781
|
+
kind = "api";
|
|
782
|
+
pathPrefix = "/api";
|
|
783
|
+
} else if (rel.startsWith("server/routes/")) {
|
|
784
|
+
kind = "route";
|
|
785
|
+
} else if (rel.startsWith("server/middleware/")) {
|
|
786
|
+
kind = "middleware";
|
|
787
|
+
} else {
|
|
788
|
+
kind = "plugin";
|
|
789
|
+
}
|
|
790
|
+
const inner = rel.replace(/^server\/(api|routes|middleware|plugins)\//, "");
|
|
791
|
+
const { path: subPath, method } = nitroPathFromFile(inner);
|
|
792
|
+
routes.push({
|
|
793
|
+
path: kind === "middleware" || kind === "plugin" ? `(global: ${inner.replace(/\.(ts|js|mts|mjs)$/, "")})` : pathPrefix + subPath,
|
|
794
|
+
file: rel,
|
|
795
|
+
absolutePath: file,
|
|
796
|
+
method: kind === "api" || kind === "route" ? method : "*",
|
|
797
|
+
kind,
|
|
798
|
+
middleware: kind === "middleware" || kind === "plugin" ? [] : globalMiddleware
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
802
|
+
}
|
|
803
|
+
function isNuxtModule(name) {
|
|
804
|
+
return name.startsWith("@nuxt/") || name.startsWith("nuxt-") || name.includes("nuxt") || name.startsWith("@nuxtjs/") || name === "nitro" || name === "@pinia/nuxt";
|
|
805
|
+
}
|
|
806
|
+
async function listModules(root, configValue) {
|
|
807
|
+
const result = [];
|
|
808
|
+
const seen = /* @__PURE__ */ new Set();
|
|
809
|
+
const configModules = configValue["modules"] ?? [];
|
|
810
|
+
const configModuleNames = /* @__PURE__ */ new Set();
|
|
811
|
+
for (const mod of configModules) {
|
|
812
|
+
if (typeof mod === "string") {
|
|
813
|
+
configModuleNames.add(mod);
|
|
814
|
+
seen.add(mod);
|
|
815
|
+
result.push({ name: mod, status: "active", source: "nuxt.config", local: false, config: void 0 });
|
|
816
|
+
} else if (Array.isArray(mod) && typeof mod[0] === "string") {
|
|
817
|
+
configModuleNames.add(mod[0]);
|
|
818
|
+
seen.add(mod[0]);
|
|
819
|
+
result.push({ name: mod[0], status: "active", source: "nuxt.config", local: false, config: mod[1] });
|
|
820
|
+
} else if (typeof mod === "function") {
|
|
821
|
+
const name = mod.name || "(local module)";
|
|
822
|
+
configModuleNames.add(name);
|
|
823
|
+
seen.add(name);
|
|
824
|
+
result.push({ name, status: "active", source: "nuxt.config", local: true, config: void 0 });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
const pkgRaw = await readIfExists(path2.join(root, "package.json"));
|
|
829
|
+
if (pkgRaw) {
|
|
830
|
+
const pkg = JSON.parse(pkgRaw);
|
|
831
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
832
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
833
|
+
if (!isNuxtModule(name)) continue;
|
|
834
|
+
const status = configModuleNames.has(name) ? "active" : "inactive";
|
|
835
|
+
if (seen.has(name)) {
|
|
836
|
+
const existing = result.find((m) => m.name === name);
|
|
837
|
+
if (existing) {
|
|
838
|
+
existing.version = version;
|
|
839
|
+
existing.source = status === "active" ? "both" : "package.json";
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
seen.add(name);
|
|
843
|
+
result.push({ name, version, status, source: "package.json", local: false, config: void 0 });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
for (const mod of result) {
|
|
850
|
+
if (!mod.version) {
|
|
851
|
+
const pkgPath = path2.join(root, "node_modules", ...mod.name.split("/"), "package.json");
|
|
852
|
+
const raw = await readIfExists(pkgPath);
|
|
853
|
+
if (raw) {
|
|
854
|
+
try {
|
|
855
|
+
mod.version = JSON.parse(raw).version;
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
862
|
+
}
|
|
863
|
+
async function searchCode(root, query, fileTypes, maxResults = 50) {
|
|
864
|
+
const patterns = fileTypes && fileTypes.length > 0 ? fileTypes.map((ext) => `**/*${ext.startsWith(".") ? ext : "." + ext}`) : ["**/*"];
|
|
865
|
+
const files = await findFiles(root, patterns, [
|
|
866
|
+
"**/package-lock.json",
|
|
867
|
+
"**/yarn.lock",
|
|
868
|
+
"**/pnpm-lock.yaml",
|
|
869
|
+
"**/bun.lockb"
|
|
870
|
+
]);
|
|
871
|
+
const matches = [];
|
|
872
|
+
const regex = new RegExp(escapeRegExp(query), "i");
|
|
873
|
+
for (const file of files) {
|
|
874
|
+
const ext = path2.extname(file).toLowerCase();
|
|
875
|
+
if (!TEXT_EXTENSIONS.has(ext)) continue;
|
|
876
|
+
const content = await readIfExists(file);
|
|
877
|
+
if (!content) continue;
|
|
878
|
+
const lines = content.split("\n");
|
|
879
|
+
for (let i = 0; i < lines.length; i++) {
|
|
880
|
+
const line = lines[i];
|
|
881
|
+
const m = regex.exec(line);
|
|
882
|
+
if (!m) continue;
|
|
883
|
+
const start = Math.max(0, i - 1);
|
|
884
|
+
const end = Math.min(lines.length - 1, i + 1);
|
|
885
|
+
matches.push({
|
|
886
|
+
file: relativePath(root, file),
|
|
887
|
+
line: i + 1,
|
|
888
|
+
column: m.index + 1,
|
|
889
|
+
lineText: line.trim(),
|
|
890
|
+
context: lines.slice(start, end + 1)
|
|
891
|
+
});
|
|
892
|
+
if (matches.length >= maxResults) return matches;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return matches;
|
|
896
|
+
}
|
|
897
|
+
function escapeRegExp(text) {
|
|
898
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
899
|
+
}
|
|
900
|
+
async function getNuxtVersion(root) {
|
|
901
|
+
const candidates = [
|
|
902
|
+
path2.join(root, "node_modules", "nuxt", "package.json"),
|
|
903
|
+
path2.join(root, "package.json")
|
|
904
|
+
];
|
|
905
|
+
for (const file of candidates) {
|
|
906
|
+
const raw = await readIfExists(file);
|
|
907
|
+
if (!raw) continue;
|
|
908
|
+
try {
|
|
909
|
+
const pkg = JSON.parse(raw);
|
|
910
|
+
if (file.endsWith("node_modules/nuxt/package.json") && typeof pkg.version === "string") {
|
|
911
|
+
return pkg.version;
|
|
912
|
+
}
|
|
913
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
914
|
+
const version = deps["nuxt"] ?? deps["nuxt-edge"];
|
|
915
|
+
if (version) return version.replace(/[\^~]/, "");
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
async function listDirectories(root) {
|
|
922
|
+
const dirs = ["pages", "components", "composables", "utils", "server", "layouts", "middleware", "plugins", "assets", "public", "content", "modules"];
|
|
923
|
+
const existing = [];
|
|
924
|
+
for (const dir of dirs) {
|
|
925
|
+
if (await exists(path2.join(root, dir))) existing.push(dir);
|
|
926
|
+
}
|
|
927
|
+
return existing;
|
|
928
|
+
}
|
|
929
|
+
async function isBuilt(root) {
|
|
930
|
+
return exists(path2.join(root, ".nuxt"));
|
|
931
|
+
}
|
|
932
|
+
async function analyzeBundleDeps(root) {
|
|
933
|
+
const packages = [];
|
|
934
|
+
const warnings = [];
|
|
935
|
+
const raw = await readIfExists(path2.join(root, "package.json"));
|
|
936
|
+
if (!raw) return { packages, warnings };
|
|
937
|
+
let pkg;
|
|
938
|
+
try {
|
|
939
|
+
pkg = JSON.parse(raw);
|
|
940
|
+
} catch {
|
|
941
|
+
return { packages, warnings };
|
|
942
|
+
}
|
|
943
|
+
const dependencies = pkg.dependencies ?? {};
|
|
944
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
945
|
+
const all = /* @__PURE__ */ new Map();
|
|
946
|
+
for (const [name, version] of Object.entries(dependencies)) all.set(name, { version, kind: "runtime" });
|
|
947
|
+
for (const [name, version] of Object.entries(devDependencies)) all.set(name, { version, kind: "dev" });
|
|
948
|
+
for (const [name, { version, kind }] of all) {
|
|
949
|
+
const isModule = isNuxtModule(name);
|
|
950
|
+
packages.push({ name, version, kind: isModule ? "nuxt-module" : kind });
|
|
951
|
+
if (isModule) {
|
|
952
|
+
if (kind === "runtime") {
|
|
953
|
+
warnings.push(`Nuxt module "${name}" is in dependencies; it should be in devDependencies.`);
|
|
954
|
+
}
|
|
955
|
+
const installed = await exists(path2.join(root, "node_modules", ...name.split("/"), "package.json"));
|
|
956
|
+
if (!installed) warnings.push(`Nuxt module "${name}" is declared but not installed in node_modules.`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
960
|
+
return { packages, warnings };
|
|
961
|
+
}
|
|
962
|
+
async function readTypeInfo(root) {
|
|
963
|
+
const result = {};
|
|
964
|
+
const importsDts = await readIfExists(path2.join(root, ".nuxt", "imports.d.ts"));
|
|
965
|
+
if (importsDts) result.importsDts = importsDts;
|
|
966
|
+
const typesDir = path2.join(root, ".nuxt", "types");
|
|
967
|
+
if (await exists(typesDir)) {
|
|
968
|
+
const files = await findFiles(root, [".nuxt/types/**/*.d.ts"]);
|
|
969
|
+
result.typesDir = files.map((f) => relativePath(root, f)).sort();
|
|
970
|
+
}
|
|
971
|
+
const schema = await readIfExists(path2.join(root, ".nuxt", "schema", "nuxt.schema.json"));
|
|
972
|
+
if (schema) result.schema = schema;
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/utils/layers.ts
|
|
977
|
+
import path3 from "path";
|
|
978
|
+
import { promises as fs3 } from "fs";
|
|
979
|
+
async function layerName(dir) {
|
|
980
|
+
try {
|
|
981
|
+
const raw = await fs3.readFile(path3.join(dir, "package.json"), "utf8");
|
|
982
|
+
const pkg = JSON.parse(raw);
|
|
983
|
+
if (pkg.name) return pkg.name;
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
return path3.basename(dir);
|
|
987
|
+
}
|
|
988
|
+
async function readExtends(configFile) {
|
|
989
|
+
if (!configFile) return [];
|
|
990
|
+
try {
|
|
991
|
+
const source = await fs3.readFile(configFile, "utf8");
|
|
992
|
+
const parsed = parseConfigSource(source);
|
|
993
|
+
const ext = parsed["extends"];
|
|
994
|
+
if (Array.isArray(ext)) return ext.filter((e) => typeof e === "string");
|
|
995
|
+
} catch {
|
|
996
|
+
}
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
async function resolveLayer(dir, counter, seen) {
|
|
1000
|
+
const abs = path3.resolve(dir);
|
|
1001
|
+
if (seen.has(abs)) return null;
|
|
1002
|
+
seen.add(abs);
|
|
1003
|
+
const configFile = findConfigFile(abs);
|
|
1004
|
+
const extendsEntries = await readExtends(configFile);
|
|
1005
|
+
const layer = {
|
|
1006
|
+
cwd: abs,
|
|
1007
|
+
name: await layerName(abs),
|
|
1008
|
+
configFile,
|
|
1009
|
+
extends: extendsEntries,
|
|
1010
|
+
priority: counter.value++,
|
|
1011
|
+
children: []
|
|
1012
|
+
};
|
|
1013
|
+
for (const entry of extendsEntries.slice().reverse()) {
|
|
1014
|
+
const child = await resolveLayer(path3.resolve(abs, entry), counter, seen);
|
|
1015
|
+
if (child) layer.children.push(child);
|
|
1016
|
+
}
|
|
1017
|
+
return layer;
|
|
1018
|
+
}
|
|
1019
|
+
async function resolveLayers(root, extendsEntries) {
|
|
1020
|
+
const absRoot = path3.resolve(root);
|
|
1021
|
+
const seen = /* @__PURE__ */ new Set([absRoot]);
|
|
1022
|
+
const counter = { value: 1 };
|
|
1023
|
+
const rootLayer = {
|
|
1024
|
+
cwd: absRoot,
|
|
1025
|
+
name: await layerName(absRoot),
|
|
1026
|
+
configFile: findConfigFile(absRoot),
|
|
1027
|
+
extends: extendsEntries,
|
|
1028
|
+
priority: 0,
|
|
1029
|
+
children: []
|
|
1030
|
+
};
|
|
1031
|
+
const result = [rootLayer];
|
|
1032
|
+
const appendTree = (layer) => {
|
|
1033
|
+
result.push(layer);
|
|
1034
|
+
for (const child of layer.children) appendTree(child);
|
|
1035
|
+
};
|
|
1036
|
+
for (const entry of extendsEntries.slice().reverse()) {
|
|
1037
|
+
const child = await resolveLayer(path3.resolve(absRoot, entry), counter, seen);
|
|
1038
|
+
if (child) {
|
|
1039
|
+
rootLayer.children.push(child);
|
|
1040
|
+
appendTree(child);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return result;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/utils/auto-imports.ts
|
|
1047
|
+
import { promises as fs4 } from "fs";
|
|
1048
|
+
import path4 from "path";
|
|
1049
|
+
async function readIfExists2(file) {
|
|
1050
|
+
try {
|
|
1051
|
+
return await fs4.readFile(file, "utf8");
|
|
1052
|
+
} catch {
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function classify(name, source) {
|
|
1057
|
+
if (source.endsWith(".vue") || source.includes("/components/")) return "component";
|
|
1058
|
+
if (/^use[A-Z]/.test(name)) return "function";
|
|
1059
|
+
if (/^[A-Z]/.test(name)) return "value";
|
|
1060
|
+
return "function";
|
|
1061
|
+
}
|
|
1062
|
+
function sourceFromType(type) {
|
|
1063
|
+
const m = type.match(/typeof\s+import\(['"]([^'"]+)['"]\)/);
|
|
1064
|
+
return m?.[1];
|
|
1065
|
+
}
|
|
1066
|
+
function parseImportsDts(content) {
|
|
1067
|
+
const result = [];
|
|
1068
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1069
|
+
const push = (name, source, signature) => {
|
|
1070
|
+
if (!name || name.startsWith("_")) return;
|
|
1071
|
+
if (seen.has(name)) return;
|
|
1072
|
+
seen.add(name);
|
|
1073
|
+
result.push({ name, source, kind: classify(name, source), signature });
|
|
1074
|
+
};
|
|
1075
|
+
for (const line of content.split("\n")) {
|
|
1076
|
+
const exportFrom = line.match(/export\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/);
|
|
1077
|
+
if (exportFrom) {
|
|
1078
|
+
const source = exportFrom[2];
|
|
1079
|
+
for (const raw of exportFrom[1].split(",")) {
|
|
1080
|
+
const item = raw.trim();
|
|
1081
|
+
if (!item) continue;
|
|
1082
|
+
const as = item.match(/^([\w$]+)\s+as\s+([\w$]+)$/);
|
|
1083
|
+
if (as) push(as[2], source);
|
|
1084
|
+
else if (item === "default") continue;
|
|
1085
|
+
else push(item, source);
|
|
1086
|
+
}
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const exportConst = line.match(/export\s+const\s+([\w$]+)\s*:\s*([^\n;]+)/);
|
|
1090
|
+
if (exportConst) {
|
|
1091
|
+
const name = exportConst[1];
|
|
1092
|
+
const type = exportConst[2].trim();
|
|
1093
|
+
push(name, sourceFromType(type) ?? "#imports", type);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const constDecl = line.match(/^\s*const\s+([\w$]+)\s*:\s*([^\n;]+)/);
|
|
1097
|
+
if (constDecl) {
|
|
1098
|
+
const name = constDecl[1];
|
|
1099
|
+
const type = constDecl[2].trim();
|
|
1100
|
+
push(name, sourceFromType(type) ?? "#imports", type);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
async function readAutoImports(root) {
|
|
1107
|
+
const candidates = [
|
|
1108
|
+
path4.join(root, ".nuxt", "imports.d.ts"),
|
|
1109
|
+
path4.join(root, ".nuxt", "types", "imports.d.ts")
|
|
1110
|
+
];
|
|
1111
|
+
for (const file of candidates) {
|
|
1112
|
+
const content = await readIfExists2(file);
|
|
1113
|
+
if (content) return { built: true, imports: parseImportsDts(content) };
|
|
1114
|
+
}
|
|
1115
|
+
return { built: false, imports: [] };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/utils/write-guard.ts
|
|
1119
|
+
var WriteGuard = class {
|
|
1120
|
+
constructor(allowWrites2) {
|
|
1121
|
+
this.allowWrites = allowWrites2;
|
|
1122
|
+
}
|
|
1123
|
+
allowWrites;
|
|
1124
|
+
/**
|
|
1125
|
+
* Decide whether a write operation may proceed.
|
|
1126
|
+
*
|
|
1127
|
+
* @param dryRun when true, no disk write is attempted — always allowed.
|
|
1128
|
+
*/
|
|
1129
|
+
authorize(dryRun) {
|
|
1130
|
+
if (dryRun) return { ok: true };
|
|
1131
|
+
if (this.allowWrites) return { ok: true };
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
error: "Write operations are disabled. Set MCP_NUXT_ALLOW_WRITES=1 or pass --allow-writes to enable real writes. Dry-run plans are always available."
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/context.ts
|
|
1140
|
+
var NuxtProjectContext = class {
|
|
1141
|
+
root;
|
|
1142
|
+
writeGuard;
|
|
1143
|
+
configCache = null;
|
|
1144
|
+
snapshotCache = null;
|
|
1145
|
+
layersCache = null;
|
|
1146
|
+
constructor(root, options = {}) {
|
|
1147
|
+
this.root = path5.resolve(root ?? detectProjectRoot(process.cwd()));
|
|
1148
|
+
this.writeGuard = new WriteGuard(options.allowWrites ?? false);
|
|
1149
|
+
}
|
|
1150
|
+
/** Resolve a path relative to the project root. */
|
|
1151
|
+
p(...segments) {
|
|
1152
|
+
return path5.join(this.root, ...segments);
|
|
1153
|
+
}
|
|
1154
|
+
async config() {
|
|
1155
|
+
if (!this.configCache) this.configCache = await loadNuxtConfig(this.root);
|
|
1156
|
+
return this.configCache;
|
|
1157
|
+
}
|
|
1158
|
+
async snapshot() {
|
|
1159
|
+
if (this.snapshotCache) return this.snapshotCache;
|
|
1160
|
+
const config = await this.config();
|
|
1161
|
+
const layers = await this.layers();
|
|
1162
|
+
const extraLayers = layers.slice(1).map((l) => ({ cwd: l.cwd, name: l.name }));
|
|
1163
|
+
const [routes, components, composables, serverRoutes, nuxtVersion, directories, built] = await Promise.all([
|
|
1164
|
+
listPageRoutes(this.root, extraLayers),
|
|
1165
|
+
listComponents(this.root, extraLayers),
|
|
1166
|
+
listComposables(this.root, extraLayers),
|
|
1167
|
+
listServerRoutes(this.root),
|
|
1168
|
+
getNuxtVersion(this.root),
|
|
1169
|
+
listDirectories(this.root),
|
|
1170
|
+
isBuilt(this.root)
|
|
1171
|
+
]);
|
|
1172
|
+
const modules = await listModules(this.root, config.value);
|
|
1173
|
+
const name = await this.projectName();
|
|
1174
|
+
const configFile = config.configPath ? path5.basename(config.configPath) : void 0;
|
|
1175
|
+
const overview = {
|
|
1176
|
+
root: this.root,
|
|
1177
|
+
name,
|
|
1178
|
+
framework: "Nuxt",
|
|
1179
|
+
nuxtVersion,
|
|
1180
|
+
hasNuxtConfig: config.configPath !== null,
|
|
1181
|
+
configFile,
|
|
1182
|
+
counts: {
|
|
1183
|
+
routes: routes.length,
|
|
1184
|
+
components: components.length,
|
|
1185
|
+
composables: composables.length,
|
|
1186
|
+
serverRoutes: serverRoutes.filter((r) => r.kind === "api" || r.kind === "route").length,
|
|
1187
|
+
middleware: serverRoutes.filter((r) => r.kind === "middleware").length,
|
|
1188
|
+
plugins: serverRoutes.filter((r) => r.kind === "plugin").length,
|
|
1189
|
+
modules: modules.filter((m) => m.status === "active").length
|
|
1190
|
+
},
|
|
1191
|
+
directories,
|
|
1192
|
+
modules: modules.map((m) => m.name),
|
|
1193
|
+
layers: layers.map((l) => l.name),
|
|
1194
|
+
built
|
|
1195
|
+
};
|
|
1196
|
+
this.snapshotCache = { routes, components, composables, serverRoutes, modules, overview };
|
|
1197
|
+
return this.snapshotCache;
|
|
1198
|
+
}
|
|
1199
|
+
async invalidate() {
|
|
1200
|
+
this.snapshotCache = null;
|
|
1201
|
+
this.configCache = null;
|
|
1202
|
+
this.layersCache = null;
|
|
1203
|
+
}
|
|
1204
|
+
/** Resolve the Nuxt extends hierarchy (highest priority first). */
|
|
1205
|
+
async layers() {
|
|
1206
|
+
if (this.layersCache) return this.layersCache;
|
|
1207
|
+
const config = await this.config();
|
|
1208
|
+
const ext = config.value["extends"];
|
|
1209
|
+
const entries = Array.isArray(ext) ? ext.filter((e) => typeof e === "string") : [];
|
|
1210
|
+
this.layersCache = await resolveLayers(this.root, entries);
|
|
1211
|
+
return this.layersCache;
|
|
1212
|
+
}
|
|
1213
|
+
/** Read type info (.nuxt/imports.d.ts, .nuxt/types, schema). */
|
|
1214
|
+
async typeInfo() {
|
|
1215
|
+
return readTypeInfo(this.root);
|
|
1216
|
+
}
|
|
1217
|
+
/** Read the parsed auto-imports registry (requires .nuxt build output). */
|
|
1218
|
+
async autoImports() {
|
|
1219
|
+
return readAutoImports(this.root);
|
|
1220
|
+
}
|
|
1221
|
+
async projectName() {
|
|
1222
|
+
try {
|
|
1223
|
+
const pkg = JSON.parse(await fs5.readFile(this.p("package.json"), "utf8"));
|
|
1224
|
+
return pkg.name ?? path5.basename(this.root);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return path5.basename(this.root);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
// src/utils/helpers.ts
|
|
1232
|
+
function textResult(text) {
|
|
1233
|
+
return { content: [{ type: "text", text }] };
|
|
1234
|
+
}
|
|
1235
|
+
function jsonResult(value) {
|
|
1236
|
+
return textResult(JSON.stringify(value, null, 2));
|
|
1237
|
+
}
|
|
1238
|
+
function errorResult(error) {
|
|
1239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1240
|
+
return textResult(JSON.stringify({ error: message }, null, 2));
|
|
1241
|
+
}
|
|
1242
|
+
function safe(fn) {
|
|
1243
|
+
return async (...args) => {
|
|
1244
|
+
try {
|
|
1245
|
+
return await fn(...args);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
return errorResult(error);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/tools/project-overview.ts
|
|
1253
|
+
function registerOverviewTools(server, ctx) {
|
|
1254
|
+
server.tool(
|
|
1255
|
+
"get_project_overview",
|
|
1256
|
+
"Get a high-level overview of the Nuxt project: root, name, Nuxt version, config file, directory layout, layer list, and counts of routes, components, composables, server routes, middleware, plugins and modules.",
|
|
1257
|
+
safe(async () => {
|
|
1258
|
+
const { overview } = await ctx.snapshot();
|
|
1259
|
+
return jsonResult(overview);
|
|
1260
|
+
})
|
|
1261
|
+
);
|
|
1262
|
+
server.tool(
|
|
1263
|
+
"get_type_info",
|
|
1264
|
+
"Read generated Nuxt type information (.nuxt/imports.d.ts, .nuxt/types files and nuxt.schema.json) when the project has been built.",
|
|
1265
|
+
safe(async () => {
|
|
1266
|
+
return jsonResult(await ctx.typeInfo());
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/tools/routes.ts
|
|
1272
|
+
import { promises as fs6 } from "fs";
|
|
1273
|
+
import path6 from "path";
|
|
1274
|
+
import { z } from "zod";
|
|
1275
|
+
function registerRouteTools(server, ctx) {
|
|
1276
|
+
server.tool(
|
|
1277
|
+
"list_routes",
|
|
1278
|
+
"List all page routes with their resolved URL paths, source files, params, middleware, layouts and children. Use this to understand the page/routing surface of the project.",
|
|
1279
|
+
{
|
|
1280
|
+
pattern: z.string().optional().describe("Optional substring filter matched against the route path or file")
|
|
1281
|
+
},
|
|
1282
|
+
safe(async ({ pattern }) => {
|
|
1283
|
+
const { routes } = await ctx.snapshot();
|
|
1284
|
+
let filtered = routes;
|
|
1285
|
+
if (pattern) {
|
|
1286
|
+
const needle = pattern.toLowerCase();
|
|
1287
|
+
filtered = routes.filter(
|
|
1288
|
+
(r) => r.path.toLowerCase().includes(needle) || r.file.toLowerCase().includes(needle)
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
return jsonResult({ count: filtered.length, total: routes.length, routes: filtered });
|
|
1292
|
+
})
|
|
1293
|
+
);
|
|
1294
|
+
server.tool(
|
|
1295
|
+
"get_route",
|
|
1296
|
+
"Get detailed information about a single page route by its resolved URL path (e.g. /users/:id).",
|
|
1297
|
+
{
|
|
1298
|
+
path: z.string().describe("The resolved route path, e.g. /users/:id")
|
|
1299
|
+
},
|
|
1300
|
+
safe(async ({ path: path11 }) => {
|
|
1301
|
+
const { routes } = await ctx.snapshot();
|
|
1302
|
+
const route = routes.find((r) => r.path === path11);
|
|
1303
|
+
if (!route) {
|
|
1304
|
+
return jsonResult({
|
|
1305
|
+
found: false,
|
|
1306
|
+
path: path11,
|
|
1307
|
+
suggestions: routes.filter((r) => r.path.includes(path11) || path11.includes(r.path)).slice(0, 10).map((r) => r.path)
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
return jsonResult({ found: true, route });
|
|
1311
|
+
})
|
|
1312
|
+
);
|
|
1313
|
+
server.tool(
|
|
1314
|
+
"add_route",
|
|
1315
|
+
"Scaffold a new page route under pages/ following Nuxt conventions. Supports static, dynamic (:id -> [id]), optional ([[id]]), and catch-all ([...slug]) segments.",
|
|
1316
|
+
{
|
|
1317
|
+
path: z.string().describe("Route path, e.g. /users/:id or /about"),
|
|
1318
|
+
layout: z.string().optional().describe("Optional layout name to set in definePageMeta"),
|
|
1319
|
+
middleware: z.array(z.string()).optional().describe("Optional middleware names to set in definePageMeta"),
|
|
1320
|
+
dryRun: z.boolean().optional().describe("When true (default), return the planned file without writing")
|
|
1321
|
+
},
|
|
1322
|
+
safe(async ({ path: routePath, layout, middleware, dryRun }) => {
|
|
1323
|
+
const auth = ctx.writeGuard.authorize(dryRun ?? true);
|
|
1324
|
+
if (!auth.ok) return jsonResult(auth);
|
|
1325
|
+
const file = ctx.p(routePathToFile(routePath));
|
|
1326
|
+
const meta = [];
|
|
1327
|
+
if (layout) meta.push(` layout: '${layout}'`);
|
|
1328
|
+
if (middleware && middleware.length > 0) {
|
|
1329
|
+
meta.push(` middleware: [${middleware.map((m) => `'${m}'`).join(", ")}]`);
|
|
1330
|
+
}
|
|
1331
|
+
const metaBlock = meta.length > 0 ? `
|
|
1332
|
+
definePageMeta({
|
|
1333
|
+
${meta.join(",\n")}
|
|
1334
|
+
})` : "";
|
|
1335
|
+
const content = `<script setup>${metaBlock}
|
|
1336
|
+
</script>
|
|
1337
|
+
|
|
1338
|
+
<template>
|
|
1339
|
+
<div>${routePath}</div>
|
|
1340
|
+
</template>
|
|
1341
|
+
`;
|
|
1342
|
+
if (dryRun ?? true) return jsonResult({ written: false, dryRun: true, path: file, content });
|
|
1343
|
+
await fs6.mkdir(path6.dirname(file), { recursive: true });
|
|
1344
|
+
await fs6.writeFile(file, content, "utf8");
|
|
1345
|
+
return jsonResult({ written: true, path: file });
|
|
1346
|
+
})
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/tools/components.ts
|
|
1351
|
+
import { promises as fs7 } from "fs";
|
|
1352
|
+
import path7 from "path";
|
|
1353
|
+
import { z as z2 } from "zod";
|
|
1354
|
+
function registerComponentTools(server, ctx) {
|
|
1355
|
+
server.tool(
|
|
1356
|
+
"list_components",
|
|
1357
|
+
"List all Vue components with their auto-imported names, file paths, props, emits and slots. Use this to discover existing UI building blocks before writing new code.",
|
|
1358
|
+
{
|
|
1359
|
+
name: z2.string().optional().describe("Optional substring filter on component name"),
|
|
1360
|
+
directory: z2.string().optional().describe("Optional directory filter (e.g. components)")
|
|
1361
|
+
},
|
|
1362
|
+
safe(async ({ name, directory }) => {
|
|
1363
|
+
const { components } = await ctx.snapshot();
|
|
1364
|
+
let filtered = components;
|
|
1365
|
+
if (name) filtered = filtered.filter((c) => c.name.toLowerCase().includes(name.toLowerCase()));
|
|
1366
|
+
if (directory) filtered = filtered.filter((c) => c.directory === directory);
|
|
1367
|
+
return jsonResult({ count: filtered.length, total: components.length, components: filtered });
|
|
1368
|
+
})
|
|
1369
|
+
);
|
|
1370
|
+
server.tool(
|
|
1371
|
+
"inspect_component",
|
|
1372
|
+
"Get detailed information about a single component by its auto-imported name (e.g. BaseButton): props types, slots, emits, and script setup details.",
|
|
1373
|
+
{
|
|
1374
|
+
name: z2.string().describe("Auto-imported component name, e.g. BaseButton")
|
|
1375
|
+
},
|
|
1376
|
+
safe(async ({ name }) => {
|
|
1377
|
+
const { components } = await ctx.snapshot();
|
|
1378
|
+
const component = components.find((c) => c.name === name);
|
|
1379
|
+
if (!component) {
|
|
1380
|
+
return jsonResult({
|
|
1381
|
+
found: false,
|
|
1382
|
+
name,
|
|
1383
|
+
suggestions: components.filter((c) => c.name.toLowerCase().includes(name.toLowerCase())).slice(0, 10).map((c) => c.name)
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
return jsonResult({ found: true, component });
|
|
1387
|
+
})
|
|
1388
|
+
);
|
|
1389
|
+
server.tool(
|
|
1390
|
+
"generate_component",
|
|
1391
|
+
"Generate a new Vue SFC under components/ with typed defineProps/defineEmits and slots.",
|
|
1392
|
+
{
|
|
1393
|
+
name: z2.string().describe("Component name, e.g. BaseButton or ui/BaseButton"),
|
|
1394
|
+
props: z2.record(z2.string()).optional().describe('Prop name -> type, e.g. { "label": "string", "count": "number" }'),
|
|
1395
|
+
emits: z2.array(z2.string()).optional().describe("Event names to declare with defineEmits"),
|
|
1396
|
+
slots: z2.array(z2.string()).optional().describe("Slot names (default slot is always included)"),
|
|
1397
|
+
dryRun: z2.boolean().optional().describe("When true (default), return the planned file without writing")
|
|
1398
|
+
},
|
|
1399
|
+
safe(async ({ name, props, emits, slots, dryRun }) => {
|
|
1400
|
+
const auth = ctx.writeGuard.authorize(dryRun ?? true);
|
|
1401
|
+
if (!auth.ok) return jsonResult(auth);
|
|
1402
|
+
const clean = name.replace(/\.vue$/, "");
|
|
1403
|
+
const file = ctx.p("components", clean + ".vue");
|
|
1404
|
+
const propsBlock = props && Object.keys(props).length > 0 ? `
|
|
1405
|
+
const props = defineProps<{
|
|
1406
|
+
${Object.entries(props).map(([k, v]) => ` ${k}: ${v}`).join("\n")}
|
|
1407
|
+
}>()` : "";
|
|
1408
|
+
const emitsBlock = emits && emits.length > 0 ? `
|
|
1409
|
+
const emit = defineEmits<{
|
|
1410
|
+
${emits.map((e) => ` ${e}: []`).join("\n")}
|
|
1411
|
+
}>()` : "";
|
|
1412
|
+
const slotNodes = slots && slots.length > 0 ? slots.map((s) => ` <slot name="${s}" />`).join("\n") : " <slot />";
|
|
1413
|
+
const content = `<script setup lang="ts">${propsBlock}${emitsBlock}
|
|
1414
|
+
</script>
|
|
1415
|
+
|
|
1416
|
+
<template>
|
|
1417
|
+
${slotNodes}
|
|
1418
|
+
</template>
|
|
1419
|
+
`;
|
|
1420
|
+
if (dryRun ?? true) return jsonResult({ written: false, dryRun: true, path: file, content });
|
|
1421
|
+
await fs7.mkdir(path7.dirname(file), { recursive: true });
|
|
1422
|
+
await fs7.writeFile(file, content, "utf8");
|
|
1423
|
+
return jsonResult({ written: true, path: file });
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/tools/composables.ts
|
|
1429
|
+
import { z as z3 } from "zod";
|
|
1430
|
+
function registerComposableTools(server, ctx) {
|
|
1431
|
+
server.tool(
|
|
1432
|
+
"list_composables",
|
|
1433
|
+
"List all composables, both auto-imported and explicit. Returns name, file path, parameters, and return type. Use this to discover reusable logic before writing new code.",
|
|
1434
|
+
{ name: z3.string().optional().describe("Optional substring filter on composable name") },
|
|
1435
|
+
safe(async ({ name }) => {
|
|
1436
|
+
const { composables } = await ctx.snapshot();
|
|
1437
|
+
let filtered = composables;
|
|
1438
|
+
if (name) filtered = composables.filter((c) => c.name.toLowerCase().includes(name.toLowerCase()));
|
|
1439
|
+
return jsonResult({
|
|
1440
|
+
count: filtered.length,
|
|
1441
|
+
total: composables.length,
|
|
1442
|
+
autoImportedCount: composables.filter((c) => c.autoImported).length,
|
|
1443
|
+
composables: filtered
|
|
1444
|
+
});
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
server.tool(
|
|
1448
|
+
"get_auto_imports",
|
|
1449
|
+
"Read the .nuxt/imports.d.ts auto-imports registry: every auto-imported symbol with its source path and type signature. Requires the project to have been prepared/built. Use this instead of guessing import paths or function signatures.",
|
|
1450
|
+
{
|
|
1451
|
+
filter: z3.string().optional().describe("Optional substring filter on the symbol name or source path")
|
|
1452
|
+
},
|
|
1453
|
+
safe(async ({ filter }) => {
|
|
1454
|
+
const { built, imports } = await ctx.autoImports();
|
|
1455
|
+
let filtered = imports;
|
|
1456
|
+
if (filter) {
|
|
1457
|
+
const needle = filter.toLowerCase();
|
|
1458
|
+
filtered = imports.filter(
|
|
1459
|
+
(i) => i.name.toLowerCase().includes(needle) || i.source.toLowerCase().includes(needle)
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
return jsonResult({ built, count: filtered.length, total: imports.length, imports: filtered });
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/tools/server-api.ts
|
|
1468
|
+
import { promises as fs8 } from "fs";
|
|
1469
|
+
import path8 from "path";
|
|
1470
|
+
import { z as z4 } from "zod";
|
|
1471
|
+
function registerServerApiTools(server, ctx) {
|
|
1472
|
+
server.tool(
|
|
1473
|
+
"list_server_routes",
|
|
1474
|
+
"List all Nitro server API routes, middleware, and plugins. Returns route path, handler file, HTTP method, and middleware chain. Use this to understand the server/API surface of the project.",
|
|
1475
|
+
{
|
|
1476
|
+
type: z4.enum(["api", "middleware", "plugin"]).optional().describe("Filter by route kind")
|
|
1477
|
+
},
|
|
1478
|
+
safe(async ({ type }) => {
|
|
1479
|
+
const { serverRoutes } = await ctx.snapshot();
|
|
1480
|
+
let routes = serverRoutes;
|
|
1481
|
+
if (type) routes = serverRoutes.filter((r) => r.kind === type);
|
|
1482
|
+
const grouped = {
|
|
1483
|
+
api: serverRoutes.filter((r) => r.kind === "api" || r.kind === "route").length,
|
|
1484
|
+
middleware: serverRoutes.filter((r) => r.kind === "middleware").length,
|
|
1485
|
+
plugin: serverRoutes.filter((r) => r.kind === "plugin").length
|
|
1486
|
+
};
|
|
1487
|
+
return jsonResult({ count: routes.length, totals: grouped, routes });
|
|
1488
|
+
})
|
|
1489
|
+
);
|
|
1490
|
+
server.tool(
|
|
1491
|
+
"scaffold_nitro_api",
|
|
1492
|
+
"Generate a new Nitro server API handler under server/api/ with H3 defineEventHandler and optional Zod body validation.",
|
|
1493
|
+
{
|
|
1494
|
+
path: z4.string().describe("API path, e.g. /api/hello or /api/users/:id"),
|
|
1495
|
+
method: z4.enum(["get", "post", "put", "patch", "delete"]).optional().describe("HTTP method (default: handle all methods)"),
|
|
1496
|
+
zod: z4.boolean().optional().describe("Include readValidatedBody with a Zod schema (default false)"),
|
|
1497
|
+
dryRun: z4.boolean().optional().describe("When true (default), return the planned file without writing")
|
|
1498
|
+
},
|
|
1499
|
+
safe(async ({ path: apiPath, method, zod, dryRun }) => {
|
|
1500
|
+
const auth = ctx.writeGuard.authorize(dryRun ?? true);
|
|
1501
|
+
if (!auth.ok) return jsonResult(auth);
|
|
1502
|
+
const file = ctx.p(routePathToNitroFile(apiPath, method));
|
|
1503
|
+
const content = zod ? "import { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n const body = await readValidatedBody(event, z.object({}).passthrough().safeParse)\n if (!body.success) throw createError({ statusCode: 400, statusMessage: 'Invalid body' })\n return body.data\n})\n" : "export default defineEventHandler((event) => {\n return { ok: true }\n})\n";
|
|
1504
|
+
if (dryRun ?? true) return jsonResult({ written: false, dryRun: true, path: file, content });
|
|
1505
|
+
await fs8.mkdir(path8.dirname(file), { recursive: true });
|
|
1506
|
+
await fs8.writeFile(file, content, "utf8");
|
|
1507
|
+
return jsonResult({ written: true, path: file });
|
|
1508
|
+
})
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/tools/modules.ts
|
|
1513
|
+
import { promises as fs9 } from "fs";
|
|
1514
|
+
import { z as z5 } from "zod";
|
|
1515
|
+
function registerModuleTools(server, ctx) {
|
|
1516
|
+
server.tool(
|
|
1517
|
+
"list_modules",
|
|
1518
|
+
"List Nuxt modules discovered from nuxt.config and package.json, with version, status (active/inactive) and source. Use this to understand which Nuxt features are available.",
|
|
1519
|
+
{
|
|
1520
|
+
status: z5.enum(["active", "inactive"]).optional().describe("Filter by module status")
|
|
1521
|
+
},
|
|
1522
|
+
safe(async ({ status }) => {
|
|
1523
|
+
const { modules } = await ctx.snapshot();
|
|
1524
|
+
let filtered = modules;
|
|
1525
|
+
if (status) filtered = modules.filter((m) => m.status === status);
|
|
1526
|
+
return jsonResult({
|
|
1527
|
+
count: filtered.length,
|
|
1528
|
+
active: modules.filter((m) => m.status === "active").length,
|
|
1529
|
+
modules: filtered
|
|
1530
|
+
});
|
|
1531
|
+
})
|
|
1532
|
+
);
|
|
1533
|
+
server.tool(
|
|
1534
|
+
"add_nuxt_module",
|
|
1535
|
+
"Add a Nuxt module to the modules array in nuxt.config, optionally with options. Returns a diff; dry-run by default.",
|
|
1536
|
+
{
|
|
1537
|
+
name: z5.string().describe("Module name, e.g. @nuxtjs/i18n"),
|
|
1538
|
+
options: z5.string().optional().describe('Optional module options as a JSON object string, e.g. {"locales": ["en"]}'),
|
|
1539
|
+
dryRun: z5.boolean().optional().describe("When true (default), return the planned change without writing")
|
|
1540
|
+
},
|
|
1541
|
+
safe(async ({ name, options, dryRun }) => {
|
|
1542
|
+
const auth = ctx.writeGuard.authorize(dryRun ?? true);
|
|
1543
|
+
if (!auth.ok) return jsonResult(auth);
|
|
1544
|
+
const config = await ctx.config();
|
|
1545
|
+
if (!config.configPath || !config.source) {
|
|
1546
|
+
return jsonResult({ ok: false, error: "No writable nuxt.config file found." });
|
|
1547
|
+
}
|
|
1548
|
+
let parsedOptions;
|
|
1549
|
+
if (options !== void 0) {
|
|
1550
|
+
try {
|
|
1551
|
+
parsedOptions = JSON.parse(options);
|
|
1552
|
+
} catch {
|
|
1553
|
+
return jsonResult({ ok: false, error: "options must be a valid JSON object string" });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const { newSource, diff } = appendModule(config.source, name, parsedOptions);
|
|
1557
|
+
if (dryRun ?? true) {
|
|
1558
|
+
return jsonResult({ ok: true, written: false, dryRun: true, module: name, diff });
|
|
1559
|
+
}
|
|
1560
|
+
await fs9.writeFile(config.configPath, newSource, "utf8");
|
|
1561
|
+
await ctx.invalidate();
|
|
1562
|
+
return jsonResult({ ok: true, written: true, module: name, diff });
|
|
1563
|
+
})
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/tools/pages.ts
|
|
1568
|
+
import { promises as fs10 } from "fs";
|
|
1569
|
+
import { z as z6 } from "zod";
|
|
1570
|
+
function registerPageTools(server, ctx) {
|
|
1571
|
+
server.tool(
|
|
1572
|
+
"inspect_route_meta",
|
|
1573
|
+
"Inspect a single route: returns route params, middleware, layout, page meta plus the component props, emits and slots. Accepts a route path (e.g. /users/:id) or a page file path.",
|
|
1574
|
+
{
|
|
1575
|
+
path: z6.string().describe("Route path (e.g. /users/:id) or page file (e.g. pages/users/[id].vue)")
|
|
1576
|
+
},
|
|
1577
|
+
safe(async ({ path: path11 }) => {
|
|
1578
|
+
const { routes } = await ctx.snapshot();
|
|
1579
|
+
const route = routes.find((r) => r.path === path11 || r.file === path11);
|
|
1580
|
+
if (!route) {
|
|
1581
|
+
return jsonResult({
|
|
1582
|
+
found: false,
|
|
1583
|
+
path: path11,
|
|
1584
|
+
suggestions: routes.filter((r) => r.path.includes(path11) || path11.includes(r.path)).slice(0, 10).map((r) => r.path)
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
const content = await fs10.readFile(route.absolutePath, "utf8");
|
|
1588
|
+
const { meta, middleware, layout } = extractPageMeta(content);
|
|
1589
|
+
const { props, emits, slots, hasScriptSetup } = await parseVueComponent(route.absolutePath);
|
|
1590
|
+
return jsonResult({
|
|
1591
|
+
found: true,
|
|
1592
|
+
route: { ...route, meta, middleware, layout },
|
|
1593
|
+
component: { props, emits, slots, hasScriptSetup },
|
|
1594
|
+
sourceLength: content.length
|
|
1595
|
+
});
|
|
1596
|
+
})
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/tools/config.ts
|
|
1601
|
+
import { promises as fs11 } from "fs";
|
|
1602
|
+
import { z as z7 } from "zod";
|
|
1603
|
+
function registerConfigTools(server, ctx) {
|
|
1604
|
+
server.tool(
|
|
1605
|
+
"read_config",
|
|
1606
|
+
"Read the resolved Nuxt config, or a single key by dot-notation (e.g. runtimeConfig.public.apiBase). Omit the key to return the whole config.",
|
|
1607
|
+
{
|
|
1608
|
+
key: z7.string().optional().describe("Dot-notation config key to read")
|
|
1609
|
+
},
|
|
1610
|
+
safe(async ({ key }) => {
|
|
1611
|
+
const config = await ctx.config();
|
|
1612
|
+
return jsonResult({
|
|
1613
|
+
configFile: config.configPath,
|
|
1614
|
+
sourceKind: config.sourceKind,
|
|
1615
|
+
error: config.error,
|
|
1616
|
+
value: getConfigValue(config.value, key)
|
|
1617
|
+
});
|
|
1618
|
+
})
|
|
1619
|
+
);
|
|
1620
|
+
server.tool(
|
|
1621
|
+
"update_config",
|
|
1622
|
+
"Update a nuxt.config value by dot-notation key. Returns a diff of the change. Dry-run by default; real writes require the server to be started with write authorization.",
|
|
1623
|
+
{
|
|
1624
|
+
key: z7.string().describe("Dot-notation config key, e.g. ssr or runtimeConfig.public.apiBase"),
|
|
1625
|
+
value: z7.string().describe(`New value as JSON or a JS expression, e.g. "false" or "'https://api.example.com'"`),
|
|
1626
|
+
dryRun: z7.boolean().optional().describe("When true (default), return the planned change without writing")
|
|
1627
|
+
},
|
|
1628
|
+
safe(async ({ key, value, dryRun }) => {
|
|
1629
|
+
const auth = ctx.writeGuard.authorize(dryRun ?? true);
|
|
1630
|
+
if (!auth.ok) return jsonResult(auth);
|
|
1631
|
+
const config = await ctx.config();
|
|
1632
|
+
if (!config.configPath || !config.source) {
|
|
1633
|
+
return jsonResult({ ok: false, error: "No writable nuxt.config file found." });
|
|
1634
|
+
}
|
|
1635
|
+
let parsed;
|
|
1636
|
+
let raw;
|
|
1637
|
+
try {
|
|
1638
|
+
parsed = JSON.parse(value);
|
|
1639
|
+
} catch {
|
|
1640
|
+
raw = value;
|
|
1641
|
+
}
|
|
1642
|
+
const { newSource, diff } = setConfigValue(config.source, key, parsed, raw);
|
|
1643
|
+
if (dryRun ?? true) {
|
|
1644
|
+
return jsonResult({ ok: true, written: false, dryRun: true, key, configFile: config.configPath, diff });
|
|
1645
|
+
}
|
|
1646
|
+
await fs11.writeFile(config.configPath, newSource, "utf8");
|
|
1647
|
+
await ctx.invalidate();
|
|
1648
|
+
return jsonResult({ ok: true, written: true, key, configFile: config.configPath, diff });
|
|
1649
|
+
})
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/tools/search.ts
|
|
1654
|
+
import { z as z8 } from "zod";
|
|
1655
|
+
|
|
1656
|
+
// src/data/nuxt-docs.ts
|
|
1657
|
+
var DOCS = [
|
|
1658
|
+
{
|
|
1659
|
+
topic: "routing",
|
|
1660
|
+
title: "File-based Routing",
|
|
1661
|
+
body: "Nuxt maps files under pages/ to routes. `pages/index.vue` -> `/`, `pages/about.vue` -> `/about`, `pages/users/[id].vue` -> `/users/:id` (dynamic), `pages/users/[[id]].vue` -> `/users/:id?` (optional), and `pages/docs/[...slug].vue` -> `/docs/:slug(.*)*` (catch-all). Nested routes use a parent file with a same-named directory plus child pages. Use definePageMeta({ layout, middleware, ... }) to attach layout, middleware and route meta.",
|
|
1662
|
+
related: ["components", "layers"]
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
topic: "nitro",
|
|
1666
|
+
title: "Nitro Server Engine",
|
|
1667
|
+
body: "Files under server/api/ become Nitro API endpoints. `server/api/hello.get.ts` handles GET `/api/hello`; the method suffix (get/post/put/patch/delete) is stripped from the URL. `server/middleware/` runs before every request, and `server/plugins/` run once at server startup. Handlers use H3: `defineEventHandler(event)`, `readBody(event)`, `readValidatedBody(event, schema.safeParse)`, and `createError({ statusCode, statusMessage })`.",
|
|
1668
|
+
related: ["routing"]
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
topic: "modules",
|
|
1672
|
+
title: "Nuxt Modules",
|
|
1673
|
+
body: "Modules are added via the `modules` array in nuxt.config, e.g. `modules: ['@nuxtjs/i18n']` or `['@nuxtjs/i18n', { locales: ['en'] }]`. They run at build time and can register components, composables, plugins, and config. Keep Nuxt modules in devDependencies, not dependencies. Local modules can be functions: `modules: [function () { ... }]`.",
|
|
1674
|
+
related: ["config"]
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
topic: "layers",
|
|
1678
|
+
title: "Nuxt Layers (extends)",
|
|
1679
|
+
body: "`extends: ['../layer-a', '../layer-b']` composes multiple Nuxt projects into one app. Later entries override earlier ones, and the local project overrides all layers. Components, pages, composables, and config resolve across layers by priority; auto-imports and assets follow the same override chain. Use layers to share UI kits or split a monolith.",
|
|
1680
|
+
related: ["routing", "components", "config"]
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
topic: "composables",
|
|
1684
|
+
title: "Composables & Auto-imports",
|
|
1685
|
+
body: "Files in composables/ (and utils/) are auto-imported when they export a `useXxx` function. `composables/useFoo.ts` exports `useFoo` and is usable anywhere without an import statement. Nuxt also auto-imports Vue APIs (ref, computed, watch) and lifecycle hooks. The registry lives in `.nuxt/imports.d.ts` after `nuxi prepare`.",
|
|
1686
|
+
related: ["components"]
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
topic: "config",
|
|
1690
|
+
title: "Nuxt Configuration",
|
|
1691
|
+
body: "nuxt.config.ts exports `defineNuxtConfig({ ... })`. Common keys: `ssr`, `modules`, `extends`, `runtimeConfig.public` (exposed to the client), `nitro`, `app.head`, and `css`. Runtime config is read with `useRuntimeConfig()`. Prefer `runtimeConfig` over hard-coded env access so values are typed and overridable.",
|
|
1692
|
+
related: ["modules", "layers"]
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
topic: "components",
|
|
1696
|
+
title: "Vue Components",
|
|
1697
|
+
body: 'Files in components/ are auto-imported by their PascalCase path, e.g. `components/base/BaseButton.vue` -> `<BaseButton>`. Use `<script setup lang="ts">` with `defineProps<{...}>()` for typed props and `defineEmits<{...}>()` for events. Slots are declared with `<slot name="...">`; the unnamed slot is the default slot.',
|
|
1698
|
+
related: ["composables"]
|
|
1699
|
+
}
|
|
1700
|
+
];
|
|
1701
|
+
function getNuxtDocs(topic) {
|
|
1702
|
+
const t = topic.trim().toLowerCase();
|
|
1703
|
+
return DOCS.find((d) => d.topic === t || d.title.toLowerCase().includes(t)) ?? null;
|
|
1704
|
+
}
|
|
1705
|
+
function listNuxtDocsTopics() {
|
|
1706
|
+
return DOCS.map((d) => d.topic);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// src/tools/search.ts
|
|
1710
|
+
function registerSearchTools(server, ctx) {
|
|
1711
|
+
server.tool(
|
|
1712
|
+
"search_code",
|
|
1713
|
+
"Search project source files for a substring or regex. Returns matching files with line/column numbers and surrounding context lines.",
|
|
1714
|
+
{
|
|
1715
|
+
query: z8.string().describe("Substring or regex to search for"),
|
|
1716
|
+
fileTypes: z8.array(z8.string()).optional().describe('Optional file extensions, e.g. ["vue", "ts"]'),
|
|
1717
|
+
maxResults: z8.number().int().min(1).max(200).optional().describe("Maximum number of results (default 50)")
|
|
1718
|
+
},
|
|
1719
|
+
safe(async ({ query, fileTypes, maxResults }) => {
|
|
1720
|
+
const matches = await searchCode(ctx.root, query, fileTypes, maxResults ?? 50);
|
|
1721
|
+
return jsonResult({ query, count: matches.length, matches });
|
|
1722
|
+
})
|
|
1723
|
+
);
|
|
1724
|
+
server.tool(
|
|
1725
|
+
"get_nuxt_docs",
|
|
1726
|
+
"Get targeted offline Nuxt documentation guidance for a feature area (routing, nitro, modules, layers, composables, config, components).",
|
|
1727
|
+
{
|
|
1728
|
+
topic: z8.string().describe("Feature topic, e.g. routing, nitro, modules, layers")
|
|
1729
|
+
},
|
|
1730
|
+
safe(async ({ topic }) => {
|
|
1731
|
+
const doc = getNuxtDocs(topic);
|
|
1732
|
+
if (!doc) return jsonResult({ found: false, topic, available: listNuxtDocsTopics() });
|
|
1733
|
+
return jsonResult({ found: true, ...doc });
|
|
1734
|
+
})
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/tools/layers.ts
|
|
1739
|
+
function registerLayerTools(server, ctx) {
|
|
1740
|
+
server.tool(
|
|
1741
|
+
"inspect_layers",
|
|
1742
|
+
"Resolve and return the full Nuxt extends hierarchy (layers). Returns layers ordered by override priority (project first), with each layer's config file, raw extends, and sub-layers.",
|
|
1743
|
+
safe(async () => {
|
|
1744
|
+
const layers = await ctx.layers();
|
|
1745
|
+
return jsonResult({ count: layers.length, layers });
|
|
1746
|
+
})
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/utils/tsc-checker.ts
|
|
1751
|
+
import { spawn } from "child_process";
|
|
1752
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1753
|
+
import path9 from "path";
|
|
1754
|
+
function parseTscOutput(output) {
|
|
1755
|
+
const diagnostics = [];
|
|
1756
|
+
for (const line of output.split("\n")) {
|
|
1757
|
+
const m = line.match(/^([^(]+)\((\d+),(\d+)\):\s*(error|warning)\s+TS(\d+):\s+(.+)$/);
|
|
1758
|
+
if (!m) continue;
|
|
1759
|
+
diagnostics.push({
|
|
1760
|
+
file: m[1].trim() || void 0,
|
|
1761
|
+
line: Number(m[2]),
|
|
1762
|
+
column: Number(m[3]),
|
|
1763
|
+
severity: m[4] === "error" ? "error" : "warning",
|
|
1764
|
+
code: Number(m[5]),
|
|
1765
|
+
message: m[6].trim()
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
return diagnostics;
|
|
1769
|
+
}
|
|
1770
|
+
function findTsc(root) {
|
|
1771
|
+
const tsBin = path9.join(root, "node_modules", "typescript", "bin", "tsc");
|
|
1772
|
+
if (existsSync2(tsBin)) return { cmd: process.execPath, args: [tsBin] };
|
|
1773
|
+
const bin = path9.join(root, "node_modules", ".bin", "tsc");
|
|
1774
|
+
if (existsSync2(bin)) return { cmd: bin, args: [] };
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
async function runTscCheck(root) {
|
|
1778
|
+
const tsconfigs = [path9.join(root, ".nuxt", "tsconfig.json"), path9.join(root, "tsconfig.json")];
|
|
1779
|
+
const tsconfig = tsconfigs.find((f) => existsSync2(f));
|
|
1780
|
+
if (!tsconfig) return { ran: false, diagnostics: [] };
|
|
1781
|
+
const tsc = findTsc(root);
|
|
1782
|
+
if (!tsc) return { ran: false, diagnostics: [] };
|
|
1783
|
+
const args = [...tsc.args, "--noEmit", "-p", tsconfig];
|
|
1784
|
+
const output = await new Promise((resolve2, reject) => {
|
|
1785
|
+
const child = spawn(tsc.cmd, args, { cwd: root });
|
|
1786
|
+
let stdout = "";
|
|
1787
|
+
let stderr = "";
|
|
1788
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1789
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1790
|
+
child.on("close", () => resolve2(`${stdout}
|
|
1791
|
+
${stderr}`));
|
|
1792
|
+
child.on("error", reject);
|
|
1793
|
+
});
|
|
1794
|
+
return {
|
|
1795
|
+
ran: true,
|
|
1796
|
+
diagnostics: parseTscOutput(output),
|
|
1797
|
+
command: [tsc.cmd, ...args].join(" ")
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/tools/diagnostics.ts
|
|
1802
|
+
function registerDiagnosticTools(server, ctx) {
|
|
1803
|
+
server.tool(
|
|
1804
|
+
"check_type_errors",
|
|
1805
|
+
"Run a TypeScript type check against .nuxt/tsconfig.json (or the project tsconfig.json) and return structured diagnostics with file, line, column, code and severity.",
|
|
1806
|
+
safe(async () => {
|
|
1807
|
+
const result = await runTscCheck(ctx.root);
|
|
1808
|
+
return jsonResult({
|
|
1809
|
+
ran: result.ran,
|
|
1810
|
+
errors: result.diagnostics.filter((d) => d.severity === "error").length,
|
|
1811
|
+
warnings: result.diagnostics.filter((d) => d.severity === "warning").length,
|
|
1812
|
+
command: result.command,
|
|
1813
|
+
diagnostics: result.diagnostics
|
|
1814
|
+
});
|
|
1815
|
+
})
|
|
1816
|
+
);
|
|
1817
|
+
server.tool(
|
|
1818
|
+
"analyze_bundle_deps",
|
|
1819
|
+
"Analyze package.json dependencies: classify Nuxt modules vs runtime/dev dependencies and surface warnings for misplaced or missing modules.",
|
|
1820
|
+
safe(async () => {
|
|
1821
|
+
return jsonResult(await analyzeBundleDeps(ctx.root));
|
|
1822
|
+
})
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/tools/index.ts
|
|
1827
|
+
function registerAllTools(server, ctx) {
|
|
1828
|
+
registerOverviewTools(server, ctx);
|
|
1829
|
+
registerRouteTools(server, ctx);
|
|
1830
|
+
registerComponentTools(server, ctx);
|
|
1831
|
+
registerComposableTools(server, ctx);
|
|
1832
|
+
registerServerApiTools(server, ctx);
|
|
1833
|
+
registerModuleTools(server, ctx);
|
|
1834
|
+
registerPageTools(server, ctx);
|
|
1835
|
+
registerConfigTools(server, ctx);
|
|
1836
|
+
registerSearchTools(server, ctx);
|
|
1837
|
+
registerLayerTools(server, ctx);
|
|
1838
|
+
registerDiagnosticTools(server, ctx);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/resources/layers-graph.ts
|
|
1842
|
+
var LAYERS_URI = "nuxt://project/layers";
|
|
1843
|
+
function registerLayersResource(server, ctx) {
|
|
1844
|
+
server.registerResource(
|
|
1845
|
+
"project-layers",
|
|
1846
|
+
LAYERS_URI,
|
|
1847
|
+
{
|
|
1848
|
+
title: "Nuxt Layers",
|
|
1849
|
+
description: "Resolved extends hierarchy with override priority order",
|
|
1850
|
+
mimeType: "application/json"
|
|
1851
|
+
},
|
|
1852
|
+
async () => {
|
|
1853
|
+
const layers = await ctx.layers();
|
|
1854
|
+
return {
|
|
1855
|
+
contents: [
|
|
1856
|
+
{
|
|
1857
|
+
uri: LAYERS_URI,
|
|
1858
|
+
mimeType: "application/json",
|
|
1859
|
+
text: JSON.stringify({ count: layers.length, layers }, null, 2)
|
|
1860
|
+
}
|
|
1861
|
+
]
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/resources/auto-imports.ts
|
|
1868
|
+
var AUTO_IMPORTS_URI = "nuxt://project/auto-imports";
|
|
1869
|
+
var TYPES_URI = "nuxt://project/types";
|
|
1870
|
+
function registerAutoImportsResource(server, ctx) {
|
|
1871
|
+
server.registerResource(
|
|
1872
|
+
"project-auto-imports",
|
|
1873
|
+
AUTO_IMPORTS_URI,
|
|
1874
|
+
{
|
|
1875
|
+
title: "Nuxt Auto-imports Registry",
|
|
1876
|
+
description: "Every auto-imported symbol with source path and type signature (from .nuxt/imports.d.ts)",
|
|
1877
|
+
mimeType: "application/json"
|
|
1878
|
+
},
|
|
1879
|
+
async () => {
|
|
1880
|
+
const { built, imports } = await ctx.autoImports();
|
|
1881
|
+
return {
|
|
1882
|
+
contents: [
|
|
1883
|
+
{
|
|
1884
|
+
uri: AUTO_IMPORTS_URI,
|
|
1885
|
+
mimeType: "application/json",
|
|
1886
|
+
text: JSON.stringify({ built, count: imports.length, imports }, null, 2)
|
|
1887
|
+
}
|
|
1888
|
+
]
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
);
|
|
1892
|
+
server.registerResource(
|
|
1893
|
+
"project-types",
|
|
1894
|
+
TYPES_URI,
|
|
1895
|
+
{
|
|
1896
|
+
title: "Nuxt Generated Types",
|
|
1897
|
+
description: "Raw generated type info (.nuxt/imports.d.ts, .nuxt/types file list, nuxt.schema.json)",
|
|
1898
|
+
mimeType: "application/json"
|
|
1899
|
+
},
|
|
1900
|
+
async () => {
|
|
1901
|
+
const info = await ctx.typeInfo();
|
|
1902
|
+
return {
|
|
1903
|
+
contents: [
|
|
1904
|
+
{
|
|
1905
|
+
uri: TYPES_URI,
|
|
1906
|
+
mimeType: "application/json",
|
|
1907
|
+
text: JSON.stringify(info, null, 2)
|
|
1908
|
+
}
|
|
1909
|
+
]
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/resources/project-structure.ts
|
|
1916
|
+
var OVERVIEW_URI = "nuxt://project/overview";
|
|
1917
|
+
function registerProjectStructureResource(server, ctx) {
|
|
1918
|
+
server.registerResource(
|
|
1919
|
+
"project-overview",
|
|
1920
|
+
OVERVIEW_URI,
|
|
1921
|
+
{
|
|
1922
|
+
title: "Nuxt Project Overview",
|
|
1923
|
+
description: "High-level overview of the Nuxt project structure and counts",
|
|
1924
|
+
mimeType: "application/json"
|
|
1925
|
+
},
|
|
1926
|
+
async () => {
|
|
1927
|
+
const { overview } = await ctx.snapshot();
|
|
1928
|
+
return {
|
|
1929
|
+
contents: [
|
|
1930
|
+
{
|
|
1931
|
+
uri: OVERVIEW_URI,
|
|
1932
|
+
mimeType: "application/json",
|
|
1933
|
+
text: JSON.stringify(overview, null, 2)
|
|
1934
|
+
}
|
|
1935
|
+
]
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/resources/route-map.ts
|
|
1942
|
+
var ROUTES_URI = "nuxt://project/routes";
|
|
1943
|
+
function registerRouteMapResource(server, ctx) {
|
|
1944
|
+
server.registerResource(
|
|
1945
|
+
"project-routes",
|
|
1946
|
+
ROUTES_URI,
|
|
1947
|
+
{
|
|
1948
|
+
title: "Nuxt Route Map",
|
|
1949
|
+
description: "Full resolved route map with files, params, middleware and layouts",
|
|
1950
|
+
mimeType: "application/json"
|
|
1951
|
+
},
|
|
1952
|
+
async () => {
|
|
1953
|
+
const { routes } = await ctx.snapshot();
|
|
1954
|
+
return {
|
|
1955
|
+
contents: [
|
|
1956
|
+
{
|
|
1957
|
+
uri: ROUTES_URI,
|
|
1958
|
+
mimeType: "application/json",
|
|
1959
|
+
text: JSON.stringify({ count: routes.length, routes }, null, 2)
|
|
1960
|
+
}
|
|
1961
|
+
]
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/resources/nuxt-config.ts
|
|
1968
|
+
var CONFIG_URI = "nuxt://project/config";
|
|
1969
|
+
function registerNuxtConfigResource(server, ctx) {
|
|
1970
|
+
server.registerResource(
|
|
1971
|
+
"project-config",
|
|
1972
|
+
CONFIG_URI,
|
|
1973
|
+
{
|
|
1974
|
+
title: "Nuxt Config",
|
|
1975
|
+
description: "Current resolved Nuxt configuration object",
|
|
1976
|
+
mimeType: "application/json"
|
|
1977
|
+
},
|
|
1978
|
+
async () => {
|
|
1979
|
+
const config = await ctx.config();
|
|
1980
|
+
return {
|
|
1981
|
+
contents: [
|
|
1982
|
+
{
|
|
1983
|
+
uri: CONFIG_URI,
|
|
1984
|
+
mimeType: "application/json",
|
|
1985
|
+
text: JSON.stringify(
|
|
1986
|
+
{
|
|
1987
|
+
configFile: config.configPath,
|
|
1988
|
+
sourceKind: config.sourceKind,
|
|
1989
|
+
error: config.error,
|
|
1990
|
+
value: config.value
|
|
1991
|
+
},
|
|
1992
|
+
null,
|
|
1993
|
+
2
|
|
1994
|
+
)
|
|
1995
|
+
}
|
|
1996
|
+
]
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// src/resources/module-graph.ts
|
|
2003
|
+
var MODULES_URI = "nuxt://project/modules";
|
|
2004
|
+
function registerModuleGraphResource(server, ctx) {
|
|
2005
|
+
server.registerResource(
|
|
2006
|
+
"project-modules",
|
|
2007
|
+
MODULES_URI,
|
|
2008
|
+
{
|
|
2009
|
+
title: "Nuxt Modules",
|
|
2010
|
+
description: "Installed Nuxt modules with status, source and configuration",
|
|
2011
|
+
mimeType: "application/json"
|
|
2012
|
+
},
|
|
2013
|
+
async () => {
|
|
2014
|
+
const { modules } = await ctx.snapshot();
|
|
2015
|
+
return {
|
|
2016
|
+
contents: [
|
|
2017
|
+
{
|
|
2018
|
+
uri: MODULES_URI,
|
|
2019
|
+
mimeType: "application/json",
|
|
2020
|
+
text: JSON.stringify({ count: modules.length, modules }, null, 2)
|
|
2021
|
+
}
|
|
2022
|
+
]
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/resources/index.ts
|
|
2029
|
+
function registerAllResources(server, ctx) {
|
|
2030
|
+
registerProjectStructureResource(server, ctx);
|
|
2031
|
+
registerRouteMapResource(server, ctx);
|
|
2032
|
+
registerNuxtConfigResource(server, ctx);
|
|
2033
|
+
registerModuleGraphResource(server, ctx);
|
|
2034
|
+
registerLayersResource(server, ctx);
|
|
2035
|
+
registerAutoImportsResource(server, ctx);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/utils/file-watcher.ts
|
|
2039
|
+
import { EventEmitter } from "events";
|
|
2040
|
+
import { promises as fs12 } from "fs";
|
|
2041
|
+
import path10 from "path";
|
|
2042
|
+
var ProjectWatcher = class extends EventEmitter {
|
|
2043
|
+
constructor(root, intervalMs = 2e3) {
|
|
2044
|
+
super();
|
|
2045
|
+
this.root = root;
|
|
2046
|
+
this.intervalMs = intervalMs;
|
|
2047
|
+
}
|
|
2048
|
+
root;
|
|
2049
|
+
intervalMs;
|
|
2050
|
+
timers = /* @__PURE__ */ new Map();
|
|
2051
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
2052
|
+
running = false;
|
|
2053
|
+
async start() {
|
|
2054
|
+
if (this.running) return;
|
|
2055
|
+
this.running = true;
|
|
2056
|
+
await this.scan();
|
|
2057
|
+
this.loop();
|
|
2058
|
+
}
|
|
2059
|
+
stop() {
|
|
2060
|
+
this.running = false;
|
|
2061
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
2062
|
+
this.timers.clear();
|
|
2063
|
+
}
|
|
2064
|
+
async loop() {
|
|
2065
|
+
while (this.running) {
|
|
2066
|
+
await new Promise((resolve2) => setTimeout(resolve2, this.intervalMs));
|
|
2067
|
+
await this.scan();
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async scan() {
|
|
2071
|
+
const files = await this.walk(this.root);
|
|
2072
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2073
|
+
for (const file of files) {
|
|
2074
|
+
seen.add(file);
|
|
2075
|
+
const before = this.snapshots.get(file);
|
|
2076
|
+
const after = await this.hash(file);
|
|
2077
|
+
if (before === void 0) {
|
|
2078
|
+
this.snapshots.set(file, after);
|
|
2079
|
+
this.emit("change", { type: "add", path: file });
|
|
2080
|
+
} else if (before !== after) {
|
|
2081
|
+
this.snapshots.set(file, after);
|
|
2082
|
+
this.emit("change", { type: "change", path: file });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
for (const [file] of this.snapshots) {
|
|
2086
|
+
if (!seen.has(file)) {
|
|
2087
|
+
this.snapshots.delete(file);
|
|
2088
|
+
this.emit("change", { type: "unlink", path: file });
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
async walk(dir) {
|
|
2093
|
+
const out = [];
|
|
2094
|
+
let entries;
|
|
2095
|
+
try {
|
|
2096
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
2097
|
+
} catch {
|
|
2098
|
+
return out;
|
|
2099
|
+
}
|
|
2100
|
+
for (const entry of entries) {
|
|
2101
|
+
if (entry.name === "node_modules" || entry.name === ".nuxt" || entry.name === ".output" || entry.name === ".git" || entry.name === "dist") continue;
|
|
2102
|
+
const full = path10.join(dir, entry.name);
|
|
2103
|
+
if (entry.isDirectory()) out.push(...await this.walk(full));
|
|
2104
|
+
else if (entry.isFile()) out.push(full);
|
|
2105
|
+
}
|
|
2106
|
+
return out;
|
|
2107
|
+
}
|
|
2108
|
+
async hash(file) {
|
|
2109
|
+
try {
|
|
2110
|
+
const stat = await fs12.stat(file);
|
|
2111
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
2112
|
+
} catch {
|
|
2113
|
+
return "missing";
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
// src/server.ts
|
|
2119
|
+
async function setupServer(server, projectPath2, options = {}) {
|
|
2120
|
+
const ctx = new NuxtProjectContext(projectPath2, options);
|
|
2121
|
+
registerAllTools(server, ctx);
|
|
2122
|
+
registerAllResources(server, ctx);
|
|
2123
|
+
const watcher = new ProjectWatcher(ctx.root);
|
|
2124
|
+
let timer;
|
|
2125
|
+
watcher.on("change", () => {
|
|
2126
|
+
clearTimeout(timer);
|
|
2127
|
+
timer = setTimeout(() => {
|
|
2128
|
+
void ctx.invalidate();
|
|
2129
|
+
}, 300);
|
|
2130
|
+
});
|
|
2131
|
+
void watcher.start();
|
|
2132
|
+
return { ctx, watcher };
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/index.ts
|
|
2136
|
+
var projectPath = resolve(process.cwd(), process.argv[2] || ".");
|
|
2137
|
+
var allowWrites = process.argv.includes("--allow-writes") || process.env.MCP_NUXT_ALLOW_WRITES === "1";
|
|
2138
|
+
async function main() {
|
|
2139
|
+
const server = new McpServer({
|
|
2140
|
+
name: "mcp-server-nuxt",
|
|
2141
|
+
version: "0.2.0"
|
|
2142
|
+
});
|
|
2143
|
+
await setupServer(server, projectPath, { allowWrites });
|
|
2144
|
+
const transport = new StdioServerTransport();
|
|
2145
|
+
await server.connect(transport);
|
|
2146
|
+
console.error(`MCP Nuxt Server started for project: ${projectPath}`);
|
|
2147
|
+
}
|
|
2148
|
+
main().catch((error) => {
|
|
2149
|
+
console.error("MCP Nuxt Server crashed:", error);
|
|
2150
|
+
process.exit(1);
|
|
2151
|
+
});
|