@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.
- package/README.md +203 -0
- package/bin/migrate.ts +60 -0
- package/dist/detect.d.ts +56 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +561 -0
- package/dist/detect.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +315 -0
- package/dist/migrate.js.map +1 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +159 -0
- package/dist/report.js.map +1 -0
- package/dist/transforms/app-ts.d.ts +29 -0
- package/dist/transforms/app-ts.d.ts.map +1 -0
- package/dist/transforms/app-ts.js +114 -0
- package/dist/transforms/app-ts.js.map +1 -0
- package/dist/transforms/barrels.d.ts +12 -0
- package/dist/transforms/barrels.d.ts.map +1 -0
- package/dist/transforms/barrels.js +103 -0
- package/dist/transforms/barrels.js.map +1 -0
- package/dist/transforms/generated.d.ts +7 -0
- package/dist/transforms/generated.d.ts.map +1 -0
- package/dist/transforms/generated.js +10 -0
- package/dist/transforms/generated.js.map +1 -0
- package/dist/transforms/routes.d.ts +49 -0
- package/dist/transforms/routes.d.ts.map +1 -0
- package/dist/transforms/routes.js +208 -0
- package/dist/transforms/routes.js.map +1 -0
- package/package.json +45 -0
- package/src/detect.ts +694 -0
- package/src/index.ts +9 -0
- package/src/migrate.ts +379 -0
- package/src/report.ts +195 -0
- package/src/transforms/app-ts.ts +144 -0
- package/src/transforms/barrels.ts +138 -0
- package/src/transforms/generated.ts +11 -0
- 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
|
+
}
|