@mandujs/mcp 0.30.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.
@@ -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
+ }