@mandujs/mcp 0.29.0 → 0.31.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/package.json +3 -3
- package/src/resources/skills/loader.ts +218 -218
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +300 -0
- package/src/server.ts +2 -1
- package/src/tools/ai-brief.ts +443 -443
- package/src/tools/ate-run.ts +13 -2
- package/src/tools/ate.ts +52 -3
- package/src/tools/brain.ts +37 -1
- package/src/tools/decisions.ts +270 -270
- package/src/tools/docs.ts +349 -0
- package/src/tools/extract-contract.ts +406 -406
- package/src/tools/guard.ts +56 -3
- package/src/tools/index.ts +4 -0
- package/src/tools/migrate-route-conventions.ts +345 -345
- package/src/tools/project.ts +128 -35
- package/src/tools/resource.ts +2 -1
- package/src/tools/rewrite-generated-barrel.ts +403 -403
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +0 -323
|
@@ -1,403 +1,403 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP tool — `mandu.refactor.rewrite_generated_barrel`
|
|
3
|
-
*
|
|
4
|
-
* Automates migration of barrel files that reach directly into
|
|
5
|
-
* `__generated__/*` paths (the #1 cause of `INVALID_GENERATED_IMPORT` guard
|
|
6
|
-
* failures) to the sanctioned `getGenerated()` accessor from
|
|
7
|
-
* `@mandujs/core/runtime`.
|
|
8
|
-
*
|
|
9
|
-
* Transform example:
|
|
10
|
-
*
|
|
11
|
-
* // BEFORE
|
|
12
|
-
* export { items } from "../__generated__/items.data";
|
|
13
|
-
*
|
|
14
|
-
* // AFTER
|
|
15
|
-
* import { getGenerated } from "@mandujs/core/runtime";
|
|
16
|
-
* declare module "@mandujs/core/runtime" {
|
|
17
|
-
* interface GeneratedRegistry { "items": typeof items; }
|
|
18
|
-
* }
|
|
19
|
-
* export const items = getGenerated("items");
|
|
20
|
-
*
|
|
21
|
-
* Behaviour:
|
|
22
|
-
* • Input: `{ dryRun?: boolean, patterns?: string[] }`. `patterns` are
|
|
23
|
-
* relative glob-like roots to scan (default: `packages/*`, `src/**`).
|
|
24
|
-
* The implementation uses a deterministic recursive walk — we do not
|
|
25
|
-
* pull in a glob dep.
|
|
26
|
-
* • Each file is parsed with a minimal, conservative regex set that only
|
|
27
|
-
* matches re-exports of the form `export { … } from "…/__generated__/…"`.
|
|
28
|
-
* Anything more exotic is reported under `skipped` with a reason.
|
|
29
|
-
* • On `dryRun: true` (default) we only return the plan. On
|
|
30
|
-
* `dryRun: false` we write files via `Bun.write`, which is atomic.
|
|
31
|
-
* • Parse / I/O errors for a single file are captured and do not abort
|
|
32
|
-
* the scan.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
36
|
-
import { readdir } from "fs/promises";
|
|
37
|
-
import path from "path";
|
|
38
|
-
|
|
39
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
-
// Types
|
|
41
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
export interface RewriteBarrelInput {
|
|
44
|
-
dryRun?: boolean;
|
|
45
|
-
patterns?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface RewriteBarrelPlanEntry {
|
|
49
|
-
file: string;
|
|
50
|
-
before: string;
|
|
51
|
-
after: string;
|
|
52
|
-
rewrites: Array<{ name: string; key: string; source: string }>;
|
|
53
|
-
appliedIf: "not-dry-run";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface RewriteBarrelSkip {
|
|
57
|
-
file: string;
|
|
58
|
-
reason: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface RewriteBarrelResult {
|
|
62
|
-
scanned: number;
|
|
63
|
-
matched: number;
|
|
64
|
-
rewritten: number;
|
|
65
|
-
skipped: RewriteBarrelSkip[];
|
|
66
|
-
plan: RewriteBarrelPlanEntry[];
|
|
67
|
-
dryRun: boolean;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
71
|
-
// Validation
|
|
72
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
function validateInput(
|
|
75
|
-
raw: Record<string, unknown>,
|
|
76
|
-
):
|
|
77
|
-
| { ok: true; value: { dryRun: boolean; patterns: string[] } }
|
|
78
|
-
| { ok: false; error: string; field: string; hint: string } {
|
|
79
|
-
const dryRun = raw.dryRun;
|
|
80
|
-
if (dryRun !== undefined && typeof dryRun !== "boolean") {
|
|
81
|
-
return {
|
|
82
|
-
ok: false,
|
|
83
|
-
error: "'dryRun' must be a boolean",
|
|
84
|
-
field: "dryRun",
|
|
85
|
-
hint: "Omit to default to true, pass false to actually write files",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const patterns = raw.patterns;
|
|
90
|
-
if (patterns !== undefined) {
|
|
91
|
-
if (!Array.isArray(patterns) || !patterns.every((p) => typeof p === "string")) {
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
error: "'patterns' must be an array of strings",
|
|
95
|
-
field: "patterns",
|
|
96
|
-
hint: "E.g. ['packages', 'src']",
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
ok: true,
|
|
103
|
-
value: {
|
|
104
|
-
dryRun: dryRun === undefined ? true : dryRun,
|
|
105
|
-
patterns:
|
|
106
|
-
patterns && Array.isArray(patterns) && patterns.length > 0
|
|
107
|
-
? (patterns as string[])
|
|
108
|
-
: ["packages", "src"],
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
114
|
-
// FS walk
|
|
115
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
const EXT_REGEX = /\.(?:ts|tsx|mts|cts)$/;
|
|
118
|
-
const IGNORE_DIRS = new Set([
|
|
119
|
-
"node_modules",
|
|
120
|
-
".mandu",
|
|
121
|
-
"dist",
|
|
122
|
-
"build",
|
|
123
|
-
".git",
|
|
124
|
-
".next",
|
|
125
|
-
"coverage",
|
|
126
|
-
"__generated__",
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
async function walk(root: string, dir: string, out: string[]): Promise<void> {
|
|
130
|
-
let entries: Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>;
|
|
131
|
-
try {
|
|
132
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
133
|
-
} catch {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
for (const e of entries) {
|
|
137
|
-
const full = path.join(dir, e.name);
|
|
138
|
-
if (e.isDirectory()) {
|
|
139
|
-
if (IGNORE_DIRS.has(e.name)) continue;
|
|
140
|
-
await walk(root, full, out);
|
|
141
|
-
} else if (e.isFile() && EXT_REGEX.test(e.name)) {
|
|
142
|
-
out.push(full);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function collectFiles(projectRoot: string, patterns: string[]): Promise<string[]> {
|
|
148
|
-
const collected = new Set<string>();
|
|
149
|
-
for (const rel of patterns) {
|
|
150
|
-
const abs = path.resolve(projectRoot, rel);
|
|
151
|
-
const files: string[] = [];
|
|
152
|
-
await walk(projectRoot, abs, files);
|
|
153
|
-
for (const f of files) collected.add(f);
|
|
154
|
-
}
|
|
155
|
-
return [...collected].sort();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
159
|
-
// Rewrite engine
|
|
160
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Regex matching a re-export of the form:
|
|
164
|
-
* export { a, b as c } from "…/__generated__/name.data";
|
|
165
|
-
*
|
|
166
|
-
* Captures:
|
|
167
|
-
* [1] = names block (e.g. `a, b as c`)
|
|
168
|
-
* [2] = source path
|
|
169
|
-
*/
|
|
170
|
-
const GENERATED_REEXPORT_REGEX =
|
|
171
|
-
/export\s*\{\s*([^}]+)\s*\}\s*from\s*["']([^"']*__generated__[^"']*)["']\s*;?/g;
|
|
172
|
-
|
|
173
|
-
/** Parse a names-block `a, b as c, default as d` → [{name, alias?}]. */
|
|
174
|
-
function parseNames(namesBlock: string): Array<{ name: string; alias?: string }> {
|
|
175
|
-
return namesBlock
|
|
176
|
-
.split(",")
|
|
177
|
-
.map((s) => s.trim())
|
|
178
|
-
.filter(Boolean)
|
|
179
|
-
.map((tok) => {
|
|
180
|
-
const asMatch = /^(\S+)\s+as\s+(\S+)$/.exec(tok);
|
|
181
|
-
if (asMatch) return { name: asMatch[1], alias: asMatch[2] };
|
|
182
|
-
return { name: tok };
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Derive a registry key from a __generated__ source path. */
|
|
187
|
-
export function deriveGeneratedKey(source: string): string {
|
|
188
|
-
// `../__generated__/items.data` → `items`
|
|
189
|
-
// `./__generated__/foo/bar.data` → `foo/bar`
|
|
190
|
-
const idx = source.indexOf("__generated__/");
|
|
191
|
-
const tail = idx >= 0 ? source.slice(idx + "__generated__/".length) : source;
|
|
192
|
-
return tail.replace(/\.(?:data|index|gen|g)$/, "").replace(/\/index$/, "");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export interface RewriteOutcome {
|
|
196
|
-
after: string;
|
|
197
|
-
rewrites: Array<{ name: string; key: string; source: string }>;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Rewrite the source of a single barrel file. Returns `null` when no
|
|
202
|
-
* `__generated__` re-export is present (nothing to do).
|
|
203
|
-
*
|
|
204
|
-
* Exported for regression testing.
|
|
205
|
-
*/
|
|
206
|
-
export function rewriteBarrelSource(before: string): RewriteOutcome | null {
|
|
207
|
-
// Fast bail: no hits at all.
|
|
208
|
-
if (!/__generated__/.test(before)) return null;
|
|
209
|
-
|
|
210
|
-
const rewrites: Array<{ name: string; key: string; source: string }> = [];
|
|
211
|
-
let matched = false;
|
|
212
|
-
|
|
213
|
-
// Build an accumulator of replacement blocks; each match becomes a
|
|
214
|
-
// declare-module + const block.
|
|
215
|
-
const replaced = before.replace(
|
|
216
|
-
GENERATED_REEXPORT_REGEX,
|
|
217
|
-
(_full, namesBlock: string, source: string) => {
|
|
218
|
-
matched = true;
|
|
219
|
-
const key = deriveGeneratedKey(source);
|
|
220
|
-
const names = parseNames(namesBlock);
|
|
221
|
-
const constDecls: string[] = [];
|
|
222
|
-
const typeLines: string[] = [];
|
|
223
|
-
for (const n of names) {
|
|
224
|
-
const exported = n.alias ?? n.name;
|
|
225
|
-
// Per re-export we emit a single registry key but re-expose each
|
|
226
|
-
// symbol as its own const. The registry key is derived from the
|
|
227
|
-
// file path — all symbols in the same re-export share it and are
|
|
228
|
-
// expected to live on the same generated artifact. When multiple
|
|
229
|
-
// symbols come from one source we destructure.
|
|
230
|
-
if (names.length === 1) {
|
|
231
|
-
constDecls.push(
|
|
232
|
-
`export const ${exported} = getGenerated(${JSON.stringify(key)});`,
|
|
233
|
-
);
|
|
234
|
-
typeLines.push(` ${JSON.stringify(key)}: typeof ${exported};`);
|
|
235
|
-
} else {
|
|
236
|
-
// First rewrite emits the shared const; subsequent destructures
|
|
237
|
-
// pull the field off of it.
|
|
238
|
-
if (constDecls.length === 0) {
|
|
239
|
-
constDecls.push(
|
|
240
|
-
`const __${sanitize(key)} = getGenerated(${JSON.stringify(key)});`,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
constDecls.push(
|
|
244
|
-
`export const ${exported} = __${sanitize(key)}.${n.name};`,
|
|
245
|
-
);
|
|
246
|
-
typeLines.push(
|
|
247
|
-
` ${JSON.stringify(key)}: { ${names
|
|
248
|
-
.map((m) => `${m.name}: typeof ${m.alias ?? m.name};`)
|
|
249
|
-
.join(" ")} };`,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
rewrites.push({ name: exported, key, source });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const dedupedTypeLines = [...new Set(typeLines)];
|
|
256
|
-
return [
|
|
257
|
-
`declare module "@mandujs/core/runtime" {`,
|
|
258
|
-
` interface GeneratedRegistry {`,
|
|
259
|
-
...dedupedTypeLines,
|
|
260
|
-
` }`,
|
|
261
|
-
`}`,
|
|
262
|
-
...constDecls,
|
|
263
|
-
].join("\n");
|
|
264
|
-
},
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
if (!matched) return null;
|
|
268
|
-
|
|
269
|
-
// Ensure a single `import { getGenerated } from "@mandujs/core/runtime"`
|
|
270
|
-
// is present. If the file already imports it, we leave it alone.
|
|
271
|
-
const hasImport =
|
|
272
|
-
/import\s*\{[^}]*\bgetGenerated\b[^}]*\}\s*from\s*["']@mandujs\/core\/runtime["']/.test(
|
|
273
|
-
replaced,
|
|
274
|
-
);
|
|
275
|
-
const after = hasImport
|
|
276
|
-
? replaced
|
|
277
|
-
: `import { getGenerated } from "@mandujs/core/runtime";\n${replaced}`;
|
|
278
|
-
|
|
279
|
-
return { after, rewrites };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function sanitize(s: string): string {
|
|
283
|
-
return s.replace(/[^A-Za-z0-9_]/g, "_");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
287
|
-
// Public handler
|
|
288
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
289
|
-
|
|
290
|
-
async function runRewrite(
|
|
291
|
-
projectRoot: string,
|
|
292
|
-
input: RewriteBarrelInput,
|
|
293
|
-
): Promise<RewriteBarrelResult | { error: string; field?: string; hint?: string }> {
|
|
294
|
-
const validated = validateInput(input as Record<string, unknown>);
|
|
295
|
-
if (!validated.ok) {
|
|
296
|
-
return { error: validated.error, field: validated.field, hint: validated.hint };
|
|
297
|
-
}
|
|
298
|
-
const { dryRun, patterns } = validated.value;
|
|
299
|
-
|
|
300
|
-
const files = await collectFiles(projectRoot, patterns);
|
|
301
|
-
|
|
302
|
-
const plan: RewriteBarrelPlanEntry[] = [];
|
|
303
|
-
const skipped: RewriteBarrelSkip[] = [];
|
|
304
|
-
let rewritten = 0;
|
|
305
|
-
|
|
306
|
-
for (const file of files) {
|
|
307
|
-
let before: string;
|
|
308
|
-
try {
|
|
309
|
-
before = await Bun.file(file).text();
|
|
310
|
-
} catch (err) {
|
|
311
|
-
skipped.push({
|
|
312
|
-
file: path.relative(projectRoot, file),
|
|
313
|
-
reason: `read failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
314
|
-
});
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let outcome: RewriteOutcome | null = null;
|
|
319
|
-
try {
|
|
320
|
-
outcome = rewriteBarrelSource(before);
|
|
321
|
-
} catch (err) {
|
|
322
|
-
skipped.push({
|
|
323
|
-
file: path.relative(projectRoot, file),
|
|
324
|
-
reason: `parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
325
|
-
});
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!outcome) continue; // no __generated__ re-export, irrelevant
|
|
330
|
-
|
|
331
|
-
const rel = path.relative(projectRoot, file);
|
|
332
|
-
plan.push({
|
|
333
|
-
file: rel,
|
|
334
|
-
before,
|
|
335
|
-
after: outcome.after,
|
|
336
|
-
rewrites: outcome.rewrites,
|
|
337
|
-
appliedIf: "not-dry-run",
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
if (!dryRun) {
|
|
341
|
-
try {
|
|
342
|
-
await Bun.write(file, outcome.after);
|
|
343
|
-
rewritten += 1;
|
|
344
|
-
} catch (err) {
|
|
345
|
-
skipped.push({
|
|
346
|
-
file: rel,
|
|
347
|
-
reason: `write failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
scanned: files.length,
|
|
355
|
-
matched: plan.length,
|
|
356
|
-
rewritten,
|
|
357
|
-
skipped,
|
|
358
|
-
plan,
|
|
359
|
-
dryRun,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
364
|
-
// MCP tool definition + handler map
|
|
365
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
366
|
-
|
|
367
|
-
export const rewriteGeneratedBarrelToolDefinitions: Tool[] = [
|
|
368
|
-
{
|
|
369
|
-
name: "mandu.refactor.rewrite_generated_barrel",
|
|
370
|
-
description:
|
|
371
|
-
"Scan the project for barrel files that re-export from `__generated__/*` paths (a `INVALID_GENERATED_IMPORT` guard violation) and rewrite each to use `getGenerated()` from `@mandujs/core/runtime` with the proper `GeneratedRegistry` module augmentation. Returns a per-file before/after plan. Dry-run by default.",
|
|
372
|
-
annotations: {
|
|
373
|
-
readOnlyHint: false,
|
|
374
|
-
destructiveHint: true,
|
|
375
|
-
},
|
|
376
|
-
inputSchema: {
|
|
377
|
-
type: "object",
|
|
378
|
-
properties: {
|
|
379
|
-
dryRun: {
|
|
380
|
-
type: "boolean",
|
|
381
|
-
description:
|
|
382
|
-
"When true (default), return the plan without writing files. When false, rewrite files atomically via Bun.write.",
|
|
383
|
-
},
|
|
384
|
-
patterns: {
|
|
385
|
-
type: "array",
|
|
386
|
-
items: { type: "string" },
|
|
387
|
-
description:
|
|
388
|
-
"Relative directory roots to scan (default: ['packages', 'src']).",
|
|
389
|
-
},
|
|
390
|
-
},
|
|
391
|
-
required: [],
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
];
|
|
395
|
-
|
|
396
|
-
export function rewriteGeneratedBarrelTools(projectRoot: string) {
|
|
397
|
-
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> =
|
|
398
|
-
{
|
|
399
|
-
"mandu.refactor.rewrite_generated_barrel": async (args) =>
|
|
400
|
-
runRewrite(projectRoot, args as RewriteBarrelInput),
|
|
401
|
-
};
|
|
402
|
-
return handlers;
|
|
403
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool — `mandu.refactor.rewrite_generated_barrel`
|
|
3
|
+
*
|
|
4
|
+
* Automates migration of barrel files that reach directly into
|
|
5
|
+
* `__generated__/*` paths (the #1 cause of `INVALID_GENERATED_IMPORT` guard
|
|
6
|
+
* failures) to the sanctioned `getGenerated()` accessor from
|
|
7
|
+
* `@mandujs/core/runtime`.
|
|
8
|
+
*
|
|
9
|
+
* Transform example:
|
|
10
|
+
*
|
|
11
|
+
* // BEFORE
|
|
12
|
+
* export { items } from "../__generated__/items.data";
|
|
13
|
+
*
|
|
14
|
+
* // AFTER
|
|
15
|
+
* import { getGenerated } from "@mandujs/core/runtime";
|
|
16
|
+
* declare module "@mandujs/core/runtime" {
|
|
17
|
+
* interface GeneratedRegistry { "items": typeof items; }
|
|
18
|
+
* }
|
|
19
|
+
* export const items = getGenerated("items");
|
|
20
|
+
*
|
|
21
|
+
* Behaviour:
|
|
22
|
+
* • Input: `{ dryRun?: boolean, patterns?: string[] }`. `patterns` are
|
|
23
|
+
* relative glob-like roots to scan (default: `packages/*`, `src/**`).
|
|
24
|
+
* The implementation uses a deterministic recursive walk — we do not
|
|
25
|
+
* pull in a glob dep.
|
|
26
|
+
* • Each file is parsed with a minimal, conservative regex set that only
|
|
27
|
+
* matches re-exports of the form `export { … } from "…/__generated__/…"`.
|
|
28
|
+
* Anything more exotic is reported under `skipped` with a reason.
|
|
29
|
+
* • On `dryRun: true` (default) we only return the plan. On
|
|
30
|
+
* `dryRun: false` we write files via `Bun.write`, which is atomic.
|
|
31
|
+
* • Parse / I/O errors for a single file are captured and do not abort
|
|
32
|
+
* the scan.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
36
|
+
import { readdir } from "fs/promises";
|
|
37
|
+
import path from "path";
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Types
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export interface RewriteBarrelInput {
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
patterns?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RewriteBarrelPlanEntry {
|
|
49
|
+
file: string;
|
|
50
|
+
before: string;
|
|
51
|
+
after: string;
|
|
52
|
+
rewrites: Array<{ name: string; key: string; source: string }>;
|
|
53
|
+
appliedIf: "not-dry-run";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RewriteBarrelSkip {
|
|
57
|
+
file: string;
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RewriteBarrelResult {
|
|
62
|
+
scanned: number;
|
|
63
|
+
matched: number;
|
|
64
|
+
rewritten: number;
|
|
65
|
+
skipped: RewriteBarrelSkip[];
|
|
66
|
+
plan: RewriteBarrelPlanEntry[];
|
|
67
|
+
dryRun: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
// Validation
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function validateInput(
|
|
75
|
+
raw: Record<string, unknown>,
|
|
76
|
+
):
|
|
77
|
+
| { ok: true; value: { dryRun: boolean; patterns: string[] } }
|
|
78
|
+
| { ok: false; error: string; field: string; hint: string } {
|
|
79
|
+
const dryRun = raw.dryRun;
|
|
80
|
+
if (dryRun !== undefined && typeof dryRun !== "boolean") {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: "'dryRun' must be a boolean",
|
|
84
|
+
field: "dryRun",
|
|
85
|
+
hint: "Omit to default to true, pass false to actually write files",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const patterns = raw.patterns;
|
|
90
|
+
if (patterns !== undefined) {
|
|
91
|
+
if (!Array.isArray(patterns) || !patterns.every((p) => typeof p === "string")) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: "'patterns' must be an array of strings",
|
|
95
|
+
field: "patterns",
|
|
96
|
+
hint: "E.g. ['packages', 'src']",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
value: {
|
|
104
|
+
dryRun: dryRun === undefined ? true : dryRun,
|
|
105
|
+
patterns:
|
|
106
|
+
patterns && Array.isArray(patterns) && patterns.length > 0
|
|
107
|
+
? (patterns as string[])
|
|
108
|
+
: ["packages", "src"],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// FS walk
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
const EXT_REGEX = /\.(?:ts|tsx|mts|cts)$/;
|
|
118
|
+
const IGNORE_DIRS = new Set([
|
|
119
|
+
"node_modules",
|
|
120
|
+
".mandu",
|
|
121
|
+
"dist",
|
|
122
|
+
"build",
|
|
123
|
+
".git",
|
|
124
|
+
".next",
|
|
125
|
+
"coverage",
|
|
126
|
+
"__generated__",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
async function walk(root: string, dir: string, out: string[]): Promise<void> {
|
|
130
|
+
let entries: Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>;
|
|
131
|
+
try {
|
|
132
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
133
|
+
} catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
const full = path.join(dir, e.name);
|
|
138
|
+
if (e.isDirectory()) {
|
|
139
|
+
if (IGNORE_DIRS.has(e.name)) continue;
|
|
140
|
+
await walk(root, full, out);
|
|
141
|
+
} else if (e.isFile() && EXT_REGEX.test(e.name)) {
|
|
142
|
+
out.push(full);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function collectFiles(projectRoot: string, patterns: string[]): Promise<string[]> {
|
|
148
|
+
const collected = new Set<string>();
|
|
149
|
+
for (const rel of patterns) {
|
|
150
|
+
const abs = path.resolve(projectRoot, rel);
|
|
151
|
+
const files: string[] = [];
|
|
152
|
+
await walk(projectRoot, abs, files);
|
|
153
|
+
for (const f of files) collected.add(f);
|
|
154
|
+
}
|
|
155
|
+
return [...collected].sort();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Rewrite engine
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Regex matching a re-export of the form:
|
|
164
|
+
* export { a, b as c } from "…/__generated__/name.data";
|
|
165
|
+
*
|
|
166
|
+
* Captures:
|
|
167
|
+
* [1] = names block (e.g. `a, b as c`)
|
|
168
|
+
* [2] = source path
|
|
169
|
+
*/
|
|
170
|
+
const GENERATED_REEXPORT_REGEX =
|
|
171
|
+
/export\s*\{\s*([^}]+)\s*\}\s*from\s*["']([^"']*__generated__[^"']*)["']\s*;?/g;
|
|
172
|
+
|
|
173
|
+
/** Parse a names-block `a, b as c, default as d` → [{name, alias?}]. */
|
|
174
|
+
function parseNames(namesBlock: string): Array<{ name: string; alias?: string }> {
|
|
175
|
+
return namesBlock
|
|
176
|
+
.split(",")
|
|
177
|
+
.map((s) => s.trim())
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.map((tok) => {
|
|
180
|
+
const asMatch = /^(\S+)\s+as\s+(\S+)$/.exec(tok);
|
|
181
|
+
if (asMatch) return { name: asMatch[1], alias: asMatch[2] };
|
|
182
|
+
return { name: tok };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Derive a registry key from a __generated__ source path. */
|
|
187
|
+
export function deriveGeneratedKey(source: string): string {
|
|
188
|
+
// `../__generated__/items.data` → `items`
|
|
189
|
+
// `./__generated__/foo/bar.data` → `foo/bar`
|
|
190
|
+
const idx = source.indexOf("__generated__/");
|
|
191
|
+
const tail = idx >= 0 ? source.slice(idx + "__generated__/".length) : source;
|
|
192
|
+
return tail.replace(/\.(?:data|index|gen|g)$/, "").replace(/\/index$/, "");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface RewriteOutcome {
|
|
196
|
+
after: string;
|
|
197
|
+
rewrites: Array<{ name: string; key: string; source: string }>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rewrite the source of a single barrel file. Returns `null` when no
|
|
202
|
+
* `__generated__` re-export is present (nothing to do).
|
|
203
|
+
*
|
|
204
|
+
* Exported for regression testing.
|
|
205
|
+
*/
|
|
206
|
+
export function rewriteBarrelSource(before: string): RewriteOutcome | null {
|
|
207
|
+
// Fast bail: no hits at all.
|
|
208
|
+
if (!/__generated__/.test(before)) return null;
|
|
209
|
+
|
|
210
|
+
const rewrites: Array<{ name: string; key: string; source: string }> = [];
|
|
211
|
+
let matched = false;
|
|
212
|
+
|
|
213
|
+
// Build an accumulator of replacement blocks; each match becomes a
|
|
214
|
+
// declare-module + const block.
|
|
215
|
+
const replaced = before.replace(
|
|
216
|
+
GENERATED_REEXPORT_REGEX,
|
|
217
|
+
(_full, namesBlock: string, source: string) => {
|
|
218
|
+
matched = true;
|
|
219
|
+
const key = deriveGeneratedKey(source);
|
|
220
|
+
const names = parseNames(namesBlock);
|
|
221
|
+
const constDecls: string[] = [];
|
|
222
|
+
const typeLines: string[] = [];
|
|
223
|
+
for (const n of names) {
|
|
224
|
+
const exported = n.alias ?? n.name;
|
|
225
|
+
// Per re-export we emit a single registry key but re-expose each
|
|
226
|
+
// symbol as its own const. The registry key is derived from the
|
|
227
|
+
// file path — all symbols in the same re-export share it and are
|
|
228
|
+
// expected to live on the same generated artifact. When multiple
|
|
229
|
+
// symbols come from one source we destructure.
|
|
230
|
+
if (names.length === 1) {
|
|
231
|
+
constDecls.push(
|
|
232
|
+
`export const ${exported} = getGenerated(${JSON.stringify(key)});`,
|
|
233
|
+
);
|
|
234
|
+
typeLines.push(` ${JSON.stringify(key)}: typeof ${exported};`);
|
|
235
|
+
} else {
|
|
236
|
+
// First rewrite emits the shared const; subsequent destructures
|
|
237
|
+
// pull the field off of it.
|
|
238
|
+
if (constDecls.length === 0) {
|
|
239
|
+
constDecls.push(
|
|
240
|
+
`const __${sanitize(key)} = getGenerated(${JSON.stringify(key)});`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
constDecls.push(
|
|
244
|
+
`export const ${exported} = __${sanitize(key)}.${n.name};`,
|
|
245
|
+
);
|
|
246
|
+
typeLines.push(
|
|
247
|
+
` ${JSON.stringify(key)}: { ${names
|
|
248
|
+
.map((m) => `${m.name}: typeof ${m.alias ?? m.name};`)
|
|
249
|
+
.join(" ")} };`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
rewrites.push({ name: exported, key, source });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const dedupedTypeLines = [...new Set(typeLines)];
|
|
256
|
+
return [
|
|
257
|
+
`declare module "@mandujs/core/runtime" {`,
|
|
258
|
+
` interface GeneratedRegistry {`,
|
|
259
|
+
...dedupedTypeLines,
|
|
260
|
+
` }`,
|
|
261
|
+
`}`,
|
|
262
|
+
...constDecls,
|
|
263
|
+
].join("\n");
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (!matched) return null;
|
|
268
|
+
|
|
269
|
+
// Ensure a single `import { getGenerated } from "@mandujs/core/runtime"`
|
|
270
|
+
// is present. If the file already imports it, we leave it alone.
|
|
271
|
+
const hasImport =
|
|
272
|
+
/import\s*\{[^}]*\bgetGenerated\b[^}]*\}\s*from\s*["']@mandujs\/core\/runtime["']/.test(
|
|
273
|
+
replaced,
|
|
274
|
+
);
|
|
275
|
+
const after = hasImport
|
|
276
|
+
? replaced
|
|
277
|
+
: `import { getGenerated } from "@mandujs/core/runtime";\n${replaced}`;
|
|
278
|
+
|
|
279
|
+
return { after, rewrites };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function sanitize(s: string): string {
|
|
283
|
+
return s.replace(/[^A-Za-z0-9_]/g, "_");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
287
|
+
// Public handler
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
async function runRewrite(
|
|
291
|
+
projectRoot: string,
|
|
292
|
+
input: RewriteBarrelInput,
|
|
293
|
+
): Promise<RewriteBarrelResult | { error: string; field?: string; hint?: string }> {
|
|
294
|
+
const validated = validateInput(input as Record<string, unknown>);
|
|
295
|
+
if (!validated.ok) {
|
|
296
|
+
return { error: validated.error, field: validated.field, hint: validated.hint };
|
|
297
|
+
}
|
|
298
|
+
const { dryRun, patterns } = validated.value;
|
|
299
|
+
|
|
300
|
+
const files = await collectFiles(projectRoot, patterns);
|
|
301
|
+
|
|
302
|
+
const plan: RewriteBarrelPlanEntry[] = [];
|
|
303
|
+
const skipped: RewriteBarrelSkip[] = [];
|
|
304
|
+
let rewritten = 0;
|
|
305
|
+
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
let before: string;
|
|
308
|
+
try {
|
|
309
|
+
before = await Bun.file(file).text();
|
|
310
|
+
} catch (err) {
|
|
311
|
+
skipped.push({
|
|
312
|
+
file: path.relative(projectRoot, file),
|
|
313
|
+
reason: `read failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let outcome: RewriteOutcome | null = null;
|
|
319
|
+
try {
|
|
320
|
+
outcome = rewriteBarrelSource(before);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
skipped.push({
|
|
323
|
+
file: path.relative(projectRoot, file),
|
|
324
|
+
reason: `parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!outcome) continue; // no __generated__ re-export, irrelevant
|
|
330
|
+
|
|
331
|
+
const rel = path.relative(projectRoot, file);
|
|
332
|
+
plan.push({
|
|
333
|
+
file: rel,
|
|
334
|
+
before,
|
|
335
|
+
after: outcome.after,
|
|
336
|
+
rewrites: outcome.rewrites,
|
|
337
|
+
appliedIf: "not-dry-run",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (!dryRun) {
|
|
341
|
+
try {
|
|
342
|
+
await Bun.write(file, outcome.after);
|
|
343
|
+
rewritten += 1;
|
|
344
|
+
} catch (err) {
|
|
345
|
+
skipped.push({
|
|
346
|
+
file: rel,
|
|
347
|
+
reason: `write failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
scanned: files.length,
|
|
355
|
+
matched: plan.length,
|
|
356
|
+
rewritten,
|
|
357
|
+
skipped,
|
|
358
|
+
plan,
|
|
359
|
+
dryRun,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
364
|
+
// MCP tool definition + handler map
|
|
365
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
export const rewriteGeneratedBarrelToolDefinitions: Tool[] = [
|
|
368
|
+
{
|
|
369
|
+
name: "mandu.refactor.rewrite_generated_barrel",
|
|
370
|
+
description:
|
|
371
|
+
"Scan the project for barrel files that re-export from `__generated__/*` paths (a `INVALID_GENERATED_IMPORT` guard violation) and rewrite each to use `getGenerated()` from `@mandujs/core/runtime` with the proper `GeneratedRegistry` module augmentation. Returns a per-file before/after plan. Dry-run by default.",
|
|
372
|
+
annotations: {
|
|
373
|
+
readOnlyHint: false,
|
|
374
|
+
destructiveHint: true,
|
|
375
|
+
},
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: "object",
|
|
378
|
+
properties: {
|
|
379
|
+
dryRun: {
|
|
380
|
+
type: "boolean",
|
|
381
|
+
description:
|
|
382
|
+
"When true (default), return the plan without writing files. When false, rewrite files atomically via Bun.write.",
|
|
383
|
+
},
|
|
384
|
+
patterns: {
|
|
385
|
+
type: "array",
|
|
386
|
+
items: { type: "string" },
|
|
387
|
+
description:
|
|
388
|
+
"Relative directory roots to scan (default: ['packages', 'src']).",
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
required: [],
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
export function rewriteGeneratedBarrelTools(projectRoot: string) {
|
|
397
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> =
|
|
398
|
+
{
|
|
399
|
+
"mandu.refactor.rewrite_generated_barrel": async (args) =>
|
|
400
|
+
runRewrite(projectRoot, args as RewriteBarrelInput),
|
|
401
|
+
};
|
|
402
|
+
return handlers;
|
|
403
|
+
}
|