@agentuity/migrate 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +203 -0
  2. package/bin/migrate.ts +60 -0
  3. package/dist/detect.d.ts +56 -0
  4. package/dist/detect.d.ts.map +1 -0
  5. package/dist/detect.js +561 -0
  6. package/dist/detect.js.map +1 -0
  7. package/dist/index.d.ts +9 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +9 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/migrate.d.ts +29 -0
  12. package/dist/migrate.d.ts.map +1 -0
  13. package/dist/migrate.js +315 -0
  14. package/dist/migrate.js.map +1 -0
  15. package/dist/report.d.ts +22 -0
  16. package/dist/report.d.ts.map +1 -0
  17. package/dist/report.js +159 -0
  18. package/dist/report.js.map +1 -0
  19. package/dist/transforms/app-ts.d.ts +29 -0
  20. package/dist/transforms/app-ts.d.ts.map +1 -0
  21. package/dist/transforms/app-ts.js +114 -0
  22. package/dist/transforms/app-ts.js.map +1 -0
  23. package/dist/transforms/barrels.d.ts +12 -0
  24. package/dist/transforms/barrels.d.ts.map +1 -0
  25. package/dist/transforms/barrels.js +103 -0
  26. package/dist/transforms/barrels.js.map +1 -0
  27. package/dist/transforms/generated.d.ts +7 -0
  28. package/dist/transforms/generated.d.ts.map +1 -0
  29. package/dist/transforms/generated.js +10 -0
  30. package/dist/transforms/generated.js.map +1 -0
  31. package/dist/transforms/routes.d.ts +49 -0
  32. package/dist/transforms/routes.d.ts.map +1 -0
  33. package/dist/transforms/routes.js +208 -0
  34. package/dist/transforms/routes.js.map +1 -0
  35. package/package.json +45 -0
  36. package/src/detect.ts +694 -0
  37. package/src/index.ts +9 -0
  38. package/src/migrate.ts +379 -0
  39. package/src/report.ts +195 -0
  40. package/src/transforms/app-ts.ts +144 -0
  41. package/src/transforms/barrels.ts +138 -0
  42. package/src/transforms/generated.ts +11 -0
  43. package/src/transforms/routes.ts +273 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Transform: app.ts
3
+ *
4
+ * Handles the following mechanical changes:
5
+ * 1. Remove bootstrapRuntimeEnv() import + call
6
+ * 2. Add migration comment for setup/shutdown (removed in v2)
7
+ *
8
+ * Note: analytics/workbench STAY in createApp() — they are not moved anywhere.
9
+ * In v2, createApp() is the single source of truth for all runtime config.
10
+ *
11
+ * We use simple string-level surgery rather than a full AST round-trip so that
12
+ * formatting and comments are preserved. The TypeScript API is used only for
13
+ * detection (already done in detect.ts); we apply regex-based patches here.
14
+ *
15
+ * COMPLEXITY GUARD: if the file contains anything we don't recognise (e.g. the
16
+ * giant generated/app.ts blob that v1 CLI wrote), we refuse to touch it and
17
+ * return a `complexityError`.
18
+ */
19
+
20
+ import type { DetectionResult } from '../detect';
21
+
22
+ export interface AppTsTransformResult {
23
+ /** The transformed source, or null if transformation was skipped */
24
+ source: string | null;
25
+ /** Set when the file is too complex for automated transformation */
26
+ complexityError?: string;
27
+ /** Informational messages about what was changed */
28
+ changes: string[];
29
+ }
30
+
31
+ /**
32
+ * Heuristics that identify the v1 *generated* app.ts (the 500-line blob the
33
+ * CLI wrote, not the user-facing app.ts). If we detect these, we bail out
34
+ * because the user probably has a non-standard setup.
35
+ */
36
+ const COMPLEXITY_MARKERS = [
37
+ 'bootstrapRuntimeEnv',
38
+ 'createBaseMiddleware',
39
+ 'createOtelMiddleware',
40
+ 'createAgentMiddleware',
41
+ 'setGlobalLogger',
42
+ 'setGlobalTracer',
43
+ 'setGlobalRouter',
44
+ 'getAppState',
45
+ 'getAppConfig',
46
+ 'getUserRouter',
47
+ 'loadBuildMetadata',
48
+ 'patchBunS3ForStorageDev',
49
+ 'createWorkbenchRouter',
50
+ 'injectAnalytics',
51
+ 'registerAnalyticsRoutes',
52
+ ];
53
+
54
+ /** How many complexity markers before we bail */
55
+ const COMPLEXITY_THRESHOLD = 3;
56
+
57
+ export function transformAppTs(source: string, detection: DetectionResult): AppTsTransformResult {
58
+ const changes: string[] = [];
59
+
60
+ // ── Complexity guard ───────────────────────────────────────────────────
61
+ const markerCount = COMPLEXITY_MARKERS.filter((m) => source.includes(m)).length;
62
+ if (markerCount >= COMPLEXITY_THRESHOLD) {
63
+ return {
64
+ source: null,
65
+ complexityError:
66
+ `app.ts appears to be the v1 CLI-generated entry file (detected ${markerCount} internal ` +
67
+ `framework markers). This file cannot be automatically migrated.\n` +
68
+ `\n` +
69
+ `Action required:\n` +
70
+ ` Replace app.ts with a clean v2 entry:\n` +
71
+ `\n` +
72
+ ` import { createApp } from '@agentuity/runtime';\n` +
73
+ ` import router from './src/api';\n` +
74
+ ` import agents from './src/agent';\n` +
75
+ `\n` +
76
+ ` const { server, logger } = await createApp({\n` +
77
+ ` router: { path: '/api', router },\n` +
78
+ ` agents,\n` +
79
+ ` });\n` +
80
+ `\n` +
81
+ ` logger.debug('Running %s', server.url);\n`,
82
+ changes: [],
83
+ };
84
+ }
85
+
86
+ let out = source;
87
+
88
+ // ── 1. Remove bootstrapRuntimeEnv import binding ──────────────────────
89
+ if (detection.bootstrapCallInAppTs) {
90
+ // Remove the named import specifier (handles both standalone and combined imports)
91
+ // e.g. import { bootstrapRuntimeEnv } from '@agentuity/runtime';
92
+ // e.g. import { createApp, bootstrapRuntimeEnv } from '@agentuity/runtime';
93
+ out = out.replace(
94
+ /import\s*\{([^}]*)\}\s*from\s*['"]@agentuity\/runtime['"]\s*;?/g,
95
+ (match, bindings: string) => {
96
+ const cleaned = bindings
97
+ .split(',')
98
+ .map((s) => s.trim())
99
+ .filter((s) => s && s !== 'bootstrapRuntimeEnv')
100
+ .join(', ');
101
+ if (!cleaned) return ''; // entire import removed
102
+ return match.replace(bindings, ` ${cleaned} `);
103
+ }
104
+ );
105
+
106
+ // Remove the standalone call — handles `await bootstrapRuntimeEnv();` with optional options
107
+ out = out.replace(/^\s*await\s+bootstrapRuntimeEnv\([^)]*\)\s*;?\s*\n?/gm, '');
108
+ out = out.replace(/^\s*bootstrapRuntimeEnv\([^)]*\)\s*;?\s*\n?/gm, '');
109
+
110
+ // Clean up any blank lines that result from the removal (max one blank line)
111
+ out = out.replace(/\n{3,}/g, '\n\n');
112
+
113
+ changes.push('Removed bootstrapRuntimeEnv() import and call');
114
+ }
115
+
116
+ // Note: analytics/workbench stay in createApp() in v2 - no migration needed
117
+
118
+ // ── 2. setup / shutdown scaffolding comment ───────────────────────────
119
+ if (detection.setupInCreateApp || detection.shutdownInCreateApp) {
120
+ // We do NOT remove setup/shutdown — they require human judgment.
121
+ // Instead, prepend a prominent comment block.
122
+ const comment =
123
+ `// ⚠️ MIGRATION REQUIRED — setup/shutdown removed in v2\n` +
124
+ `//\n` +
125
+ `// Move initialisation logic to module-level code (top of this file).\n` +
126
+ `// For cleanup, use Hono's standard patterns or Bun's process hooks:\n` +
127
+ `//\n` +
128
+ `// process.on('beforeExit', async () => {\n` +
129
+ `// // your cleanup here\n` +
130
+ `// });\n` +
131
+ `//\n` +
132
+ `// Then remove the setup and shutdown props from createApp().\n`;
133
+
134
+ // Insert just before the createApp call
135
+ // Match: const { a, b } = await createApp OR const foo = await createApp OR export default await createApp
136
+ out = out.replace(
137
+ /(const|let|var)\s+(\{[^}]+\}|\w+)\s*=\s*await\s+createApp|export default await createApp/,
138
+ `${comment}\n$&`
139
+ );
140
+ changes.push('Added migration comment for setup/shutdown — manual action required');
141
+ }
142
+
143
+ return { source: out, changes };
144
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Transform: generate/update barrel files
3
+ *
4
+ * • src/agent/index.ts — exports default array of all agent runners
5
+ * • src/api/index.ts — exports composed Hono router + AppRouter type
6
+ *
7
+ * These are generated fresh; if a file already exists and is not the empty
8
+ * v1 stub we leave it alone (detection layer already decided).
9
+ */
10
+
11
+ import { existsSync, readdirSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Agent barrel
16
+ // ---------------------------------------------------------------------------
17
+
18
+ interface AgentEntry {
19
+ importName: string; // camelCase identifier
20
+ relativePath: string; // './hello/agent'
21
+ }
22
+
23
+ function toImportName(agentDirName: string): string {
24
+ // kebab-case or snake_case → camelCase
25
+ return agentDirName
26
+ .split(/[-_]/)
27
+ .map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
28
+ .join('');
29
+ }
30
+
31
+ function findAgentDirs(agentDir: string): AgentEntry[] {
32
+ if (!existsSync(agentDir)) return [];
33
+ const entries: AgentEntry[] = [];
34
+
35
+ for (const entry of readdirSync(agentDir, { withFileTypes: true })) {
36
+ if (!entry.isDirectory()) continue;
37
+ const agentFile = join(agentDir, entry.name, 'agent.ts');
38
+ if (existsSync(agentFile)) {
39
+ entries.push({
40
+ importName: toImportName(entry.name),
41
+ relativePath: `./${entry.name}/agent`,
42
+ });
43
+ }
44
+ }
45
+
46
+ return entries.sort((a, b) => a.importName.localeCompare(b.importName));
47
+ }
48
+
49
+ export function generateAgentBarrel(projectDir: string): string | null {
50
+ const agentDir = join(projectDir, 'src', 'agent');
51
+ const agents = findAgentDirs(agentDir);
52
+
53
+ if (agents.length === 0) return null;
54
+
55
+ const imports = agents
56
+ .map(({ importName, relativePath }) => `import ${importName} from '${relativePath}';`)
57
+ .join('\n');
58
+
59
+ const exportList = agents.map(({ importName }) => `\t${importName},`).join('\n');
60
+
61
+ return `${imports}\n\n` + `const agents = [\n${exportList}\n];\n\n` + `export default agents;\n`;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // API barrel
66
+ // ---------------------------------------------------------------------------
67
+
68
+ interface RouteEntry {
69
+ importName: string; // camelCase identifier, e.g. `helloRouter`
70
+ mountPath: string; // '/hello'
71
+ relativePath: string; // './hello/route'
72
+ }
73
+
74
+ function toRouterImportName(routeDirName: string): string {
75
+ const camel = toImportName(routeDirName);
76
+ return `${camel}Router`;
77
+ }
78
+
79
+ function findRouteFiles(apiDir: string): RouteEntry[] {
80
+ if (!existsSync(apiDir)) return [];
81
+ const entries: RouteEntry[] = [];
82
+
83
+ for (const entry of readdirSync(apiDir, { withFileTypes: true })) {
84
+ if (!entry.isDirectory()) continue;
85
+ const routeFile = join(apiDir, entry.name, 'route.ts');
86
+ if (existsSync(routeFile)) {
87
+ entries.push({
88
+ importName: toRouterImportName(entry.name),
89
+ mountPath: `/${entry.name}`,
90
+ relativePath: `./${entry.name}/route`,
91
+ });
92
+ }
93
+ }
94
+
95
+ return entries.sort((a, b) => a.mountPath.localeCompare(b.mountPath));
96
+ }
97
+
98
+ export function generateApiBarrel(projectDir: string): string | null {
99
+ const apiDir = join(projectDir, 'src', 'api');
100
+ const routes = findRouteFiles(apiDir);
101
+
102
+ if (routes.length === 0) return null;
103
+
104
+ const imports = [
105
+ `import { Hono } from 'hono';`,
106
+ `import type { Env } from '@agentuity/runtime';`,
107
+ ...routes.map(
108
+ ({ importName, relativePath }) => `import ${importName} from '${relativePath}';`
109
+ ),
110
+ ].join('\n');
111
+
112
+ const chain = routes
113
+ .map(({ mountPath, importName }) => `\t.route('${mountPath}', ${importName})`)
114
+ .join('\n');
115
+
116
+ // The router MUST be built as a single chained expression so that TypeScript
117
+ // accumulates every route's schema into `typeof router` (AppRouter).
118
+ // Breaking the chain (e.g. separate `router.route(...)` statements) would
119
+ // produce an empty/incomplete AppRouter and break hc<AppRouter>() on the
120
+ // frontend.
121
+ //
122
+ // Frontend usage:
123
+ // import { hc } from 'hono/client';
124
+ // import type { AppRouter } from '../api';
125
+ // const client = hc<AppRouter>(window.location.origin + '/api');
126
+ // const res = await client.hello.$post({ json: { name: 'World' } });
127
+ return (
128
+ `${imports}\n\n` +
129
+ `// Routes are chained in a single expression so TypeScript can accumulate\n` +
130
+ `// every sub-router's schema into AppRouter — required for Hono RPC typing.\n` +
131
+ `const router = new Hono<Env>()\n${chain};\n\n` +
132
+ `// AppRouter is the fully-typed entry point for the Hono client.\n` +
133
+ `// Import it in your frontend: import type { AppRouter } from '../api';\n` +
134
+ `// Then use: hc<AppRouter>(window.location.origin + '/api')\n` +
135
+ `export type AppRouter = typeof router;\n\n` +
136
+ `export default router;\n`
137
+ );
138
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Transform: delete src/generated/
3
+ *
4
+ * The generated directory is 100% CLI-managed in v1. In v2 it is gone.
5
+ */
6
+
7
+ import { rmSync } from 'node:fs';
8
+
9
+ export function deleteGeneratedDir(generatedDir: string): void {
10
+ rmSync(generatedDir, { recursive: true, force: true });
11
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Transform: v1 route files → v2 Hono chained style
3
+ *
4
+ * v1 pattern:
5
+ * import { createRouter } from '@agentuity/runtime';
6
+ * const router = createRouter();
7
+ * router.get('/foo', handler);
8
+ * router.post('/bar', handler);
9
+ * export default router;
10
+ *
11
+ * v2 pattern:
12
+ * import { Hono } from 'hono';
13
+ * import type { Env } from '@agentuity/runtime';
14
+ *
15
+ * const router = new Hono<Env>()
16
+ * .get('/foo', handler)
17
+ * .post('/bar', handler);
18
+ *
19
+ * export default router;
20
+ *
21
+ * WHY CHAINING MATTERS — Hono RPC type inference:
22
+ * Hono accumulates route types via TypeScript's return-type inference on
23
+ * each chained call. If you break the chain (e.g. `router.get(...)` on a
24
+ * separate statement after the variable declaration), the Schema type
25
+ * parameter never accumulates new routes and `typeof router` carries no
26
+ * route information. The chained style is the ONLY way to get the full
27
+ * AppRouter type used by `hc<AppRouter>()` on the frontend.
28
+ *
29
+ * Individual route files export a typed router; the barrel (src/api/index.ts)
30
+ * composes them with `.route()` and re-exports `AppRouter = typeof router`.
31
+ * Frontend code imports that type:
32
+ *
33
+ * import { hc } from 'hono/client';
34
+ * import type { AppRouter } from '../api'; // or wherever
35
+ * const client = hc<AppRouter>(window.location.origin + '/api');
36
+ * const res = await client.hello.$post({ json: { name: 'World' } });
37
+ *
38
+ * COMPLEXITY GUARD: if the file:
39
+ * • Has more than one createRouter() call
40
+ * • Uses variable re-assignment of the router variable
41
+ * …we refuse and return a complexityError.
42
+ */
43
+
44
+ import ts from 'typescript';
45
+
46
+ function capitalize(s: string): string {
47
+ return s.charAt(0).toUpperCase() + s.slice(1);
48
+ }
49
+
50
+ export interface RouteTransformResult {
51
+ source: string | null;
52
+ complexityError?: string;
53
+ changes: string[];
54
+ }
55
+
56
+ // HTTP methods supported by Hono (chained)
57
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'all', 'route', 'use']);
58
+
59
+ interface RouterCall {
60
+ method: string;
61
+ /** Full text of `router.<method>(args)` statement */
62
+ statementText: string;
63
+ /** Just the argument list text, e.g. `'/foo', handler` */
64
+ argsText: string;
65
+ }
66
+
67
+ export function transformRouteFile(source: string): RouteTransformResult {
68
+ const changes: string[] = [];
69
+
70
+ // ── Parse ──────────────────────────────────────────────────────────────
71
+ const sf = ts.createSourceFile('route.ts', source, ts.ScriptTarget.ESNext, true);
72
+
73
+ // ── Find createRouter() variable declaration ───────────────────────────
74
+ let routerVarName: string | null = null;
75
+
76
+ ts.forEachChild(sf, (node) => {
77
+ if (
78
+ ts.isVariableStatement(node) ||
79
+ (ts.isVariableDeclarationList(node) && node.declarations)
80
+ ) {
81
+ const declList = ts.isVariableStatement(node)
82
+ ? node.declarationList.declarations
83
+ : (node as ts.VariableDeclarationList).declarations;
84
+
85
+ for (const decl of declList) {
86
+ if (
87
+ decl.initializer &&
88
+ ts.isCallExpression(decl.initializer) &&
89
+ ts.isIdentifier(decl.initializer.expression) &&
90
+ decl.initializer.expression.text === 'createRouter' &&
91
+ ts.isIdentifier(decl.name)
92
+ ) {
93
+ if (routerVarName !== null) {
94
+ return; // multiple createRouter calls
95
+ }
96
+ routerVarName = decl.name.text;
97
+ }
98
+ }
99
+ }
100
+ });
101
+
102
+ if (!routerVarName) {
103
+ // Not a v1 route file — nothing to do
104
+ return { source, changes: [] };
105
+ }
106
+
107
+ // ── Complexity checks ─────────────────────────────────────────────────
108
+
109
+ // Count how many times createRouter() is called
110
+ let createRouterCount = 0;
111
+ (source.match(/createRouter\s*\(/g) ?? []).forEach(() => createRouterCount++);
112
+ if (createRouterCount > 1) {
113
+ return {
114
+ source: null,
115
+ complexityError:
116
+ `Route file calls createRouter() ${createRouterCount} times. ` +
117
+ `Only a single top-level router variable is supported by the auto-migration.`,
118
+ changes: [],
119
+ };
120
+ }
121
+
122
+ // Check for re-assignment (after declaration)
123
+ // The initial `const router = createRouter()` is one match; any additional = re-assignment
124
+ const reassignPattern = new RegExp(`\\b${routerVarName}\\s*=(?!=)`, 'g');
125
+ const reassignMatches = source.match(reassignPattern) ?? [];
126
+ if (reassignMatches.length > 1) {
127
+ return {
128
+ source: null,
129
+ complexityError:
130
+ `Router variable '${routerVarName}' appears to be re-assigned. ` +
131
+ `This pattern cannot be automatically migrated.`,
132
+ changes: [],
133
+ };
134
+ }
135
+
136
+ // ── Collect router.<method>(...) calls ────────────────────────────────
137
+ const routerCalls: RouterCall[] = [];
138
+
139
+ ts.forEachChild(sf, (node) => {
140
+ if (!ts.isExpressionStatement(node)) return;
141
+ const expr = node.expression;
142
+ if (!ts.isCallExpression(expr)) return;
143
+ if (!ts.isPropertyAccessExpression(expr.expression)) return;
144
+ const obj = expr.expression.expression;
145
+ const method = expr.expression.name.text;
146
+
147
+ if (!ts.isIdentifier(obj) || obj.text !== routerVarName) return;
148
+ if (!HTTP_METHODS.has(method)) return;
149
+
150
+ const argsText = expr.arguments.map((a) => a.getText(sf)).join(', ');
151
+ const statementText = node.getText(sf);
152
+
153
+ routerCalls.push({ method, statementText, argsText });
154
+ });
155
+
156
+ if (routerCalls.length === 0) {
157
+ // createRouter() declared but no method calls — still rewrite the declaration
158
+ }
159
+
160
+ // ── Build replacement source ──────────────────────────────────────────
161
+
162
+ // 1. Replace `import { createRouter ... } from '@agentuity/runtime'` with
163
+ // `import { Hono } from 'hono';` + `import type { Env } from '@agentuity/runtime';`
164
+ let out = source;
165
+
166
+ // Remove createRouter from the @agentuity/runtime import
167
+ out = out.replace(
168
+ /import\s*\{([^}]*)\}\s*from\s*['"]@agentuity\/runtime['"]\s*;?/g,
169
+ (_match, bindings: string) => {
170
+ const parts = bindings
171
+ .split(',')
172
+ .map((s) => s.trim())
173
+ .filter(Boolean);
174
+
175
+ const withoutCreateRouter = parts.filter((p) => p !== 'createRouter');
176
+
177
+ const runtimeParts = withoutCreateRouter.filter((p) => !p.startsWith('type '));
178
+ const typeParts = withoutCreateRouter
179
+ .filter((p) => p.startsWith('type '))
180
+ .map((p) => p.slice('type '.length).trim());
181
+
182
+ // Always add `Env` to the type imports from @agentuity/runtime
183
+ if (!typeParts.includes('Env')) typeParts.push('Env');
184
+
185
+ const lines: string[] = [];
186
+
187
+ if (runtimeParts.length > 0) {
188
+ lines.push(`import { ${runtimeParts.join(', ')} } from '@agentuity/runtime';`);
189
+ }
190
+ if (typeParts.length > 0) {
191
+ lines.push(`import type { ${typeParts.join(', ')} } from '@agentuity/runtime';`);
192
+ }
193
+
194
+ return lines.join('\n');
195
+ }
196
+ );
197
+
198
+ // Add `import { Hono } from 'hono';` if not already present
199
+ if (!out.includes("from 'hono'") && !out.includes('from "hono"')) {
200
+ // Insert after the last import statement
201
+ out = out.replace(
202
+ /^(import\s[^;]+;?\s*\n)(?!import\s)/m,
203
+ (match) => `import { Hono } from 'hono';\n${match}`
204
+ );
205
+ } else {
206
+ // Ensure Hono is in the existing hono import
207
+ out = out.replace(
208
+ /import\s*\{([^}]*)\}\s*from\s*['"]hono['"]\s*;?/,
209
+ (_match, bindings: string) => {
210
+ const parts = bindings
211
+ .split(',')
212
+ .map((s) => s.trim())
213
+ .filter(Boolean);
214
+ if (!parts.includes('Hono')) {
215
+ parts.unshift('Hono');
216
+ }
217
+ return `import { ${parts.join(', ')} } from 'hono';`;
218
+ }
219
+ );
220
+ }
221
+
222
+ // 2. Replace `const router = createRouter();` with `const router = new Hono<Env>()`
223
+ // and fold the method calls into a chain.
224
+ const chainedCalls = routerCalls
225
+ .map(({ method, argsText }) => `\t.${method}(${argsText})`)
226
+ .join('\n');
227
+
228
+ const varDecl = `const ${routerVarName} = createRouter();`;
229
+
230
+ if (routerCalls.length > 0) {
231
+ const replacement = `const ${routerVarName} = new Hono<Env>()\n${chainedCalls};`;
232
+
233
+ // Remove individual router.<method>(...) statements
234
+ let modified = out;
235
+ for (const { statementText } of routerCalls) {
236
+ // Use a literal string replacement (not regex) to avoid special char issues
237
+ modified = modified.split(statementText).join('');
238
+ }
239
+
240
+ // Replace the createRouter() declaration
241
+ modified = modified.split(varDecl).join(replacement);
242
+
243
+ // Collapse extra blank lines
244
+ modified = modified.replace(/\n{3,}/g, '\n\n');
245
+
246
+ out = modified;
247
+ changes.push(
248
+ `Rewrote createRouter() declaration + ${routerCalls.length} method call(s) to chained Hono<Env>`
249
+ );
250
+ } else {
251
+ // No method calls — just swap the declaration
252
+ out = out.split(varDecl).join(`const ${routerVarName} = new Hono<Env>();`);
253
+ changes.push('Rewrote createRouter() declaration to new Hono<Env>()');
254
+ }
255
+
256
+ // Add `export type` for the router if not already exported.
257
+ // This lets the barrel (src/api/index.ts) compose routers in a
258
+ // fully-typed way, and downstream consumers can reference sub-router types.
259
+ const exportDefaultPattern = new RegExp(`export\\s+default\\s+${routerVarName}\\s*;?`);
260
+ if (exportDefaultPattern.test(out) && !out.includes(`export type`)) {
261
+ out = out.replace(
262
+ exportDefaultPattern,
263
+ `export type ${capitalize(routerVarName)}Type = typeof ${routerVarName};\n\nexport default ${routerVarName};`
264
+ );
265
+ changes.push(
266
+ `Added 'export type ${capitalize(routerVarName)}Type' for Hono RPC sub-router typing`
267
+ );
268
+ }
269
+
270
+ changes.push("Updated imports: added 'hono' import, replaced createRouter with Env type");
271
+
272
+ return { source: out, changes };
273
+ }