@agentuity/migrate 3.0.0-alpha.0 → 3.0.0-alpha.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/bin/migrate.ts +93 -10
- package/dist/detect-v3.d.ts +92 -0
- package/dist/detect-v3.d.ts.map +1 -0
- package/dist/detect-v3.js +675 -0
- package/dist/detect-v3.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/migrate-v3.d.ts +38 -0
- package/dist/migrate-v3.d.ts.map +1 -0
- package/dist/migrate-v3.js +448 -0
- package/dist/migrate-v3.js.map +1 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +64 -0
- package/dist/report.js.map +1 -1
- package/dist/transforms/v3/agents.d.ts +33 -0
- package/dist/transforms/v3/agents.d.ts.map +1 -0
- package/dist/transforms/v3/agents.js +335 -0
- package/dist/transforms/v3/agents.js.map +1 -0
- package/dist/transforms/v3/dev-setup.d.ts +27 -0
- package/dist/transforms/v3/dev-setup.d.ts.map +1 -0
- package/dist/transforms/v3/dev-setup.js +103 -0
- package/dist/transforms/v3/dev-setup.js.map +1 -0
- package/dist/transforms/v3/entry-point.d.ts +23 -0
- package/dist/transforms/v3/entry-point.d.ts.map +1 -0
- package/dist/transforms/v3/entry-point.js +67 -0
- package/dist/transforms/v3/entry-point.js.map +1 -0
- package/dist/transforms/v3/package-json.d.ts +28 -0
- package/dist/transforms/v3/package-json.d.ts.map +1 -0
- package/dist/transforms/v3/package-json.js +151 -0
- package/dist/transforms/v3/package-json.js.map +1 -0
- package/dist/transforms/v3/routes.d.ts +37 -0
- package/dist/transforms/v3/routes.d.ts.map +1 -0
- package/dist/transforms/v3/routes.js +146 -0
- package/dist/transforms/v3/routes.js.map +1 -0
- package/dist/transforms/v3/services.d.ts +19 -0
- package/dist/transforms/v3/services.d.ts.map +1 -0
- package/dist/transforms/v3/services.js +61 -0
- package/dist/transforms/v3/services.js.map +1 -0
- package/package.json +4 -4
- package/src/detect-v3.ts +867 -0
- package/src/index.ts +13 -0
- package/src/migrate-v3.ts +539 -0
- package/src/report.ts +86 -0
- package/src/transforms/v3/agents.ts +434 -0
- package/src/transforms/v3/dev-setup.ts +137 -0
- package/src/transforms/v3/entry-point.ts +90 -0
- package/src/transforms/v3/package-json.ts +183 -0
- package/src/transforms/v3/routes.ts +185 -0
- package/src/transforms/v3/services.ts +76 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform: createAgent() → plain exported functions
|
|
3
|
+
*
|
|
4
|
+
* For "simple" agents (handler + optional schema only), we:
|
|
5
|
+
* 1. Remove the createAgent() wrapper
|
|
6
|
+
* 2. Extract the handler as a named exported async function
|
|
7
|
+
* 3. Preserve schema validation if present
|
|
8
|
+
* 4. Replace ctx.* service access with imports from services module
|
|
9
|
+
*
|
|
10
|
+
* For "complex" agents, we add a prominent migration comment and leave
|
|
11
|
+
* the code untouched for manual review.
|
|
12
|
+
*
|
|
13
|
+
* We use a combination of regex and AST analysis — regex for the mechanical
|
|
14
|
+
* transforms (preserving formatting), AST for detection (already done in detect-v3).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import ts from 'typescript';
|
|
18
|
+
import type { AgentFile } from '../../detect-v3';
|
|
19
|
+
|
|
20
|
+
export interface AgentTransformResult {
|
|
21
|
+
/** The transformed source, or null if skipped */
|
|
22
|
+
source: string | null;
|
|
23
|
+
/** What was changed */
|
|
24
|
+
changes: string[];
|
|
25
|
+
/** If the agent is too complex for auto-migration */
|
|
26
|
+
manualRequired?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Names we use for the context parameter that should be removed.
|
|
31
|
+
*/
|
|
32
|
+
const CTX_PARAM_NAMES = ['ctx', 'context', 'c', '_ctx', '_context', '_c'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Service access patterns to rewrite.
|
|
36
|
+
* Maps ctx.serviceName → serviceName (import from services module).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Transform a simple agent file into a plain exported function.
|
|
41
|
+
*/
|
|
42
|
+
export function transformAgentFile(
|
|
43
|
+
source: string,
|
|
44
|
+
agentInfo: AgentFile,
|
|
45
|
+
servicesRelativePath: string
|
|
46
|
+
): AgentTransformResult {
|
|
47
|
+
if (agentInfo.complexity === 'complex') {
|
|
48
|
+
return addManualMigrationComment(source, agentInfo);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const changes: string[] = [];
|
|
52
|
+
|
|
53
|
+
const sourceFile = ts.createSourceFile(agentInfo.path, source, ts.ScriptTarget.ESNext, true);
|
|
54
|
+
|
|
55
|
+
// Find the createAgent() call and extract the handler
|
|
56
|
+
const extracted = extractHandlerFromCreateAgent(sourceFile, source);
|
|
57
|
+
if (!extracted) {
|
|
58
|
+
return {
|
|
59
|
+
source: null,
|
|
60
|
+
changes: ['Could not extract handler from createAgent() — manual review needed'],
|
|
61
|
+
manualRequired: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let output = source;
|
|
66
|
+
|
|
67
|
+
// Step 1: Remove the createAgent import
|
|
68
|
+
output = removeCreateAgentImport(output);
|
|
69
|
+
changes.push('Removed createAgent import from @agentuity/runtime');
|
|
70
|
+
|
|
71
|
+
// Step 2: Add services import if needed
|
|
72
|
+
if (agentInfo.ctxServices.length > 0) {
|
|
73
|
+
const serviceImports = agentInfo.ctxServices.filter((s) => s !== 'logger');
|
|
74
|
+
const needsLogger = agentInfo.ctxServices.includes('logger');
|
|
75
|
+
|
|
76
|
+
const importLines: string[] = [];
|
|
77
|
+
if (serviceImports.length > 0) {
|
|
78
|
+
importLines.push(
|
|
79
|
+
`import { ${serviceImports.join(', ')} } from '${servicesRelativePath}';`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (needsLogger) {
|
|
83
|
+
importLines.push(
|
|
84
|
+
`import { ${serviceImports.length > 0 ? '' : ''}logger } from '${servicesRelativePath}';`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Consolidate into one import if both
|
|
89
|
+
if (serviceImports.length > 0 || needsLogger) {
|
|
90
|
+
const allImports = [...agentInfo.ctxServices];
|
|
91
|
+
const importLine = `import { ${allImports.join(', ')} } from '${servicesRelativePath}';`;
|
|
92
|
+
// Insert after last import line
|
|
93
|
+
output = insertAfterImports(output, importLine);
|
|
94
|
+
changes.push(`Added services import: ${allImports.join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 3: Replace the entire createAgent() export with a plain function
|
|
99
|
+
output = replaceCreateAgentWithFunction(output, agentInfo, extracted);
|
|
100
|
+
changes.push(`Converted agent "${agentInfo.name}" to plain exported async function`);
|
|
101
|
+
|
|
102
|
+
// Step 4: Replace ctx.service with direct service references
|
|
103
|
+
for (const service of agentInfo.ctxServices) {
|
|
104
|
+
for (const ctxName of CTX_PARAM_NAMES) {
|
|
105
|
+
const pattern = new RegExp(`${ctxName}\\.${service}\\b`, 'g');
|
|
106
|
+
if (pattern.test(output)) {
|
|
107
|
+
output = output.replace(pattern, service);
|
|
108
|
+
changes.push(`Replaced ${ctxName}.${service} → ${service}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 5: Clean up — remove ctx/context parameter references that are now unused
|
|
114
|
+
// (Don't do this automatically — leave for the user to clean up)
|
|
115
|
+
|
|
116
|
+
return { source: output, changes };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* For complex agents, add a migration comment block.
|
|
121
|
+
*/
|
|
122
|
+
function addManualMigrationComment(source: string, agentInfo: AgentFile): AgentTransformResult {
|
|
123
|
+
const comment =
|
|
124
|
+
`// ⚠️ MIGRATION REQUIRED — createAgent() removed in v3\n` +
|
|
125
|
+
`//\n` +
|
|
126
|
+
`// This agent ("${agentInfo.name}") requires manual migration because:\n` +
|
|
127
|
+
`// ${agentInfo.complexityReason}\n` +
|
|
128
|
+
`//\n` +
|
|
129
|
+
`// To migrate:\n` +
|
|
130
|
+
`// 1. Extract the handler into a plain exported async function\n` +
|
|
131
|
+
`// 2. Move setup() logic to module-level initialization\n` +
|
|
132
|
+
`// 3. Replace ctx.kv/ctx.vector/etc. with imports from '../services'\n` +
|
|
133
|
+
`// 4. Replace ctx.config with direct configuration\n` +
|
|
134
|
+
`// 5. Remove event listeners (use your own event patterns)\n` +
|
|
135
|
+
`// 6. Remove the createAgent() import and wrapper\n` +
|
|
136
|
+
`//\n` +
|
|
137
|
+
`// Example after migration:\n` +
|
|
138
|
+
`// import { kv } from '../services';\n` +
|
|
139
|
+
`//\n` +
|
|
140
|
+
`// export async function ${toFunctionName(agentInfo.name)}(input: YourInputType) {\n` +
|
|
141
|
+
`// const data = await kv.get('namespace', 'key');\n` +
|
|
142
|
+
`// return { result: data };\n` +
|
|
143
|
+
`// }\n`;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
source: comment + '\n' + source,
|
|
147
|
+
changes: [`Added migration comment for complex agent "${agentInfo.name}"`],
|
|
148
|
+
manualRequired: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract the handler function body and parameters from createAgent().
|
|
154
|
+
*/
|
|
155
|
+
function extractHandlerFromCreateAgent(
|
|
156
|
+
sourceFile: ts.SourceFile,
|
|
157
|
+
_source: string
|
|
158
|
+
): {
|
|
159
|
+
handlerParams: string;
|
|
160
|
+
handlerBody: string;
|
|
161
|
+
hasSchema: boolean;
|
|
162
|
+
schemaText?: string;
|
|
163
|
+
} | null {
|
|
164
|
+
let result: {
|
|
165
|
+
handlerParams: string;
|
|
166
|
+
handlerBody: string;
|
|
167
|
+
hasSchema: boolean;
|
|
168
|
+
schemaText?: string;
|
|
169
|
+
} | null = null;
|
|
170
|
+
|
|
171
|
+
function visit(node: ts.Node) {
|
|
172
|
+
if (result) return;
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
ts.isCallExpression(node) &&
|
|
176
|
+
ts.isIdentifier(node.expression) &&
|
|
177
|
+
node.expression.text === 'createAgent'
|
|
178
|
+
) {
|
|
179
|
+
const configArg = node.arguments[1];
|
|
180
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg)) return;
|
|
181
|
+
|
|
182
|
+
let handlerNode: ts.Node | undefined;
|
|
183
|
+
let schemaNode: ts.Node | undefined;
|
|
184
|
+
|
|
185
|
+
for (const prop of configArg.properties) {
|
|
186
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
187
|
+
if (prop.name.text === 'handler') {
|
|
188
|
+
handlerNode = prop.initializer;
|
|
189
|
+
}
|
|
190
|
+
if (prop.name.text === 'schema') {
|
|
191
|
+
schemaNode = prop.initializer;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (ts.isMethodDeclaration(prop) && ts.isIdentifier(prop.name)) {
|
|
195
|
+
if (prop.name.text === 'handler') {
|
|
196
|
+
handlerNode = prop;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!handlerNode) return;
|
|
202
|
+
|
|
203
|
+
// Extract handler params and body
|
|
204
|
+
let params: ts.NodeArray<ts.ParameterDeclaration> | undefined;
|
|
205
|
+
let body: ts.Block | ts.Expression | undefined;
|
|
206
|
+
|
|
207
|
+
if (ts.isArrowFunction(handlerNode) || ts.isFunctionExpression(handlerNode)) {
|
|
208
|
+
params = handlerNode.parameters;
|
|
209
|
+
body = handlerNode.body;
|
|
210
|
+
} else if (ts.isMethodDeclaration(handlerNode)) {
|
|
211
|
+
params = handlerNode.parameters;
|
|
212
|
+
body = handlerNode.body;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!body) return;
|
|
216
|
+
|
|
217
|
+
// Get parameter text (skip ctx/context parameter, keep input)
|
|
218
|
+
const paramTexts: string[] = [];
|
|
219
|
+
if (params) {
|
|
220
|
+
for (let i = 0; i < params.length; i++) {
|
|
221
|
+
const param = params[i];
|
|
222
|
+
if (!param) continue;
|
|
223
|
+
const paramName = param.name.getText(sourceFile);
|
|
224
|
+
// Skip the first param if it's ctx/context (agent context)
|
|
225
|
+
if (i === 0 && CTX_PARAM_NAMES.includes(paramName)) continue;
|
|
226
|
+
paramTexts.push(param.getText(sourceFile));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Get body text
|
|
231
|
+
const bodyText = body.getText(sourceFile);
|
|
232
|
+
|
|
233
|
+
// Get schema text if present
|
|
234
|
+
const schemaText = schemaNode?.getText(sourceFile);
|
|
235
|
+
|
|
236
|
+
result = {
|
|
237
|
+
handlerParams: paramTexts.join(', '),
|
|
238
|
+
handlerBody: bodyText,
|
|
239
|
+
hasSchema: !!schemaNode,
|
|
240
|
+
schemaText,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
ts.forEachChild(node, visit);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
visit(sourceFile);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Remove `import { createAgent } from '@agentuity/runtime'` (and any other
|
|
253
|
+
* named imports from that module).
|
|
254
|
+
*/
|
|
255
|
+
function removeCreateAgentImport(source: string): string {
|
|
256
|
+
// Remove the entire @agentuity/runtime import line
|
|
257
|
+
// (there shouldn't be other useful imports from runtime in v3)
|
|
258
|
+
return source.replace(/import\s*\{[^}]*\}\s*from\s*['"]@agentuity\/runtime['"]\s*;?\s*\n?/g, '');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Insert an import line after the last existing import declaration.
|
|
263
|
+
* Uses AST to correctly handle multi-line imports.
|
|
264
|
+
*/
|
|
265
|
+
function insertAfterImports(source: string, importLine: string): string {
|
|
266
|
+
const sf = ts.createSourceFile('temp.ts', source, ts.ScriptTarget.ESNext, true);
|
|
267
|
+
|
|
268
|
+
let lastImportEnd = -1;
|
|
269
|
+
for (const stmt of sf.statements) {
|
|
270
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
271
|
+
lastImportEnd = stmt.getEnd();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (lastImportEnd >= 0) {
|
|
276
|
+
return (
|
|
277
|
+
source.substring(0, lastImportEnd) + '\n' + importLine + source.substring(lastImportEnd)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// No imports — add at the top
|
|
282
|
+
return importLine + '\n' + source;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Replace the createAgent() call with a plain exported async function.
|
|
287
|
+
*
|
|
288
|
+
* Uses AST node positions for precise replacement instead of regex
|
|
289
|
+
* (which can't handle nested braces in the config object).
|
|
290
|
+
*/
|
|
291
|
+
function replaceCreateAgentWithFunction(
|
|
292
|
+
source: string,
|
|
293
|
+
agentInfo: AgentFile,
|
|
294
|
+
extracted: {
|
|
295
|
+
handlerParams: string;
|
|
296
|
+
handlerBody: string;
|
|
297
|
+
hasSchema: boolean;
|
|
298
|
+
schemaText?: string;
|
|
299
|
+
}
|
|
300
|
+
): string {
|
|
301
|
+
const funcName = toFunctionName(agentInfo.name);
|
|
302
|
+
|
|
303
|
+
// Build the function signature
|
|
304
|
+
const funcSignature = `export async function ${funcName}(${extracted.handlerParams})`;
|
|
305
|
+
|
|
306
|
+
// Build the body
|
|
307
|
+
let bodyText = extracted.handlerBody;
|
|
308
|
+
// If the body is already a block { ... }, use it directly
|
|
309
|
+
// If it's a single expression (arrow function body), wrap it
|
|
310
|
+
if (!bodyText.trimStart().startsWith('{')) {
|
|
311
|
+
bodyText = `{\n\treturn ${bodyText};\n}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add schema validation at the top of the function body if schema was present
|
|
315
|
+
let schemaComment = '';
|
|
316
|
+
if (extracted.hasSchema && extracted.schemaText) {
|
|
317
|
+
schemaComment =
|
|
318
|
+
'\n// TODO: Schema validation was previously handled by createAgent().\n' +
|
|
319
|
+
'// The original schema definition was:\n' +
|
|
320
|
+
`// schema: ${extracted.schemaText.split('\n').join('\n// ')}\n` +
|
|
321
|
+
'// Consider using zod or @agentuity/schema for input validation.\n';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const replacement = schemaComment + funcSignature + ' ' + bodyText;
|
|
325
|
+
|
|
326
|
+
// Use AST to find the exact range of the createAgent() statement
|
|
327
|
+
const sourceFile = ts.createSourceFile(agentInfo.path, source, ts.ScriptTarget.ESNext, true);
|
|
328
|
+
|
|
329
|
+
let createAgentStart = -1;
|
|
330
|
+
let createAgentEnd = -1;
|
|
331
|
+
let exportDefaultVarName: string | null = null;
|
|
332
|
+
let isDirectExportDefault = false;
|
|
333
|
+
|
|
334
|
+
for (const stmt of sourceFile.statements) {
|
|
335
|
+
// Pattern 1: export default createAgent('name', { ... })
|
|
336
|
+
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
337
|
+
const expr = stmt.expression;
|
|
338
|
+
if (
|
|
339
|
+
ts.isCallExpression(expr) &&
|
|
340
|
+
ts.isIdentifier(expr.expression) &&
|
|
341
|
+
expr.expression.text === 'createAgent'
|
|
342
|
+
) {
|
|
343
|
+
createAgentStart = stmt.getStart(sourceFile);
|
|
344
|
+
createAgentEnd = stmt.getEnd();
|
|
345
|
+
isDirectExportDefault = true;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Pattern 2: const agent = createAgent('name', { ... })
|
|
351
|
+
if (ts.isVariableStatement(stmt)) {
|
|
352
|
+
const declarations = stmt.declarationList.declarations;
|
|
353
|
+
if (declarations.length === 1) {
|
|
354
|
+
const decl = declarations[0];
|
|
355
|
+
if (decl?.initializer) {
|
|
356
|
+
let callExpr: ts.CallExpression | undefined;
|
|
357
|
+
if (
|
|
358
|
+
ts.isCallExpression(decl.initializer) &&
|
|
359
|
+
ts.isIdentifier(decl.initializer.expression) &&
|
|
360
|
+
decl.initializer.expression.text === 'createAgent'
|
|
361
|
+
) {
|
|
362
|
+
callExpr = decl.initializer;
|
|
363
|
+
} else if (
|
|
364
|
+
ts.isAwaitExpression(decl.initializer) &&
|
|
365
|
+
ts.isCallExpression(decl.initializer.expression) &&
|
|
366
|
+
ts.isIdentifier(decl.initializer.expression.expression) &&
|
|
367
|
+
decl.initializer.expression.expression.text === 'createAgent'
|
|
368
|
+
) {
|
|
369
|
+
callExpr = decl.initializer.expression;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (callExpr && ts.isIdentifier(decl.name)) {
|
|
373
|
+
exportDefaultVarName = decl.name.text;
|
|
374
|
+
createAgentStart = stmt.getStart(sourceFile);
|
|
375
|
+
createAgentEnd = stmt.getEnd();
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (createAgentStart === -1) {
|
|
384
|
+
// Couldn't find the createAgent statement — return source unchanged
|
|
385
|
+
return source;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Replace the createAgent statement with the function
|
|
389
|
+
let result =
|
|
390
|
+
source.substring(0, createAgentStart) + replacement + source.substring(createAgentEnd);
|
|
391
|
+
|
|
392
|
+
// Handle the export default
|
|
393
|
+
if (isDirectExportDefault) {
|
|
394
|
+
// The export default was part of the statement we replaced,
|
|
395
|
+
// so add a separate export default
|
|
396
|
+
result += `\n\nexport default ${funcName};\n`;
|
|
397
|
+
} else if (exportDefaultVarName) {
|
|
398
|
+
// Replace `export default varName` with `export default funcName`
|
|
399
|
+
result = result.replace(
|
|
400
|
+
new RegExp(`export\\s+default\\s+${exportDefaultVarName}\\s*;?`),
|
|
401
|
+
`export default ${funcName};`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Convert kebab-case or snake_case agent name to camelCase function name.
|
|
410
|
+
*/
|
|
411
|
+
/** Service names that could clash with function names */
|
|
412
|
+
const RESERVED_SERVICE_NAMES = new Set([
|
|
413
|
+
'kv',
|
|
414
|
+
'vector',
|
|
415
|
+
'stream',
|
|
416
|
+
'queue',
|
|
417
|
+
'email',
|
|
418
|
+
'task',
|
|
419
|
+
'schedule',
|
|
420
|
+
'sandbox',
|
|
421
|
+
'logger',
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Convert agent name to a valid, non-clashing function name.
|
|
426
|
+
* Appends 'Handler' suffix if the name would clash with a service import.
|
|
427
|
+
*/
|
|
428
|
+
function toFunctionName(name: string): string {
|
|
429
|
+
const camel = name.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
430
|
+
if (RESERVED_SERVICE_NAMES.has(camel)) {
|
|
431
|
+
return camel + 'Handler';
|
|
432
|
+
}
|
|
433
|
+
return camel;
|
|
434
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform: dev setup for Hono + SPA projects.
|
|
3
|
+
*
|
|
4
|
+
* When a project has both a Hono backend (src/index.ts) and a Vite SPA
|
|
5
|
+
* (src/web/), we need to set up a dev workflow that runs both:
|
|
6
|
+
*
|
|
7
|
+
* 1. Add a Vite proxy config so /api requests go to the Hono backend
|
|
8
|
+
* 2. Set up dev scripts to run both servers concurrently
|
|
9
|
+
*
|
|
10
|
+
* In production, the buildpack handles this (static server + Hono backend).
|
|
11
|
+
* In dev, we use Vite's built-in proxy to keep it a single-port experience.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
|
|
16
|
+
const API_PORT = 3001;
|
|
17
|
+
|
|
18
|
+
export interface DevSetupResult {
|
|
19
|
+
/** Patched vite.config.ts content, or null if no changes */
|
|
20
|
+
viteConfig: string | null;
|
|
21
|
+
/** Changes made to vite.config.ts */
|
|
22
|
+
viteChanges: string[];
|
|
23
|
+
/** dev script value for package.json */
|
|
24
|
+
devScript: string;
|
|
25
|
+
/** server:api script value for package.json */
|
|
26
|
+
serverScript: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate dev setup for a Hono + Vite SPA project.
|
|
31
|
+
*/
|
|
32
|
+
export function generateDevSetup(viteConfigSource: string | null): DevSetupResult {
|
|
33
|
+
const serverScript = `PORT=${API_PORT} bun --hot src/index.ts`;
|
|
34
|
+
const devScript = `bun run server:api & vite dev`;
|
|
35
|
+
|
|
36
|
+
if (!viteConfigSource) {
|
|
37
|
+
// No vite.config.ts — just return scripts, no vite changes
|
|
38
|
+
return {
|
|
39
|
+
viteConfig: null,
|
|
40
|
+
viteChanges: [],
|
|
41
|
+
devScript: serverScript, // No SPA, just run the server
|
|
42
|
+
serverScript,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if proxy is already configured
|
|
47
|
+
if (viteConfigSource.includes('proxy')) {
|
|
48
|
+
return {
|
|
49
|
+
viteConfig: null,
|
|
50
|
+
viteChanges: ['Vite proxy already configured — skipped'],
|
|
51
|
+
devScript,
|
|
52
|
+
serverScript,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Add server.proxy to vite.config.ts using AST to find the right insertion point
|
|
57
|
+
const patched = addViteProxy(viteConfigSource);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
viteConfig: patched.source,
|
|
61
|
+
viteChanges: patched.changes,
|
|
62
|
+
devScript,
|
|
63
|
+
serverScript,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Add server.proxy configuration to vite.config.ts.
|
|
69
|
+
*
|
|
70
|
+
* Finds the defineConfig() call and injects a `server` property with proxy config.
|
|
71
|
+
*/
|
|
72
|
+
function addViteProxy(source: string): { source: string; changes: string[] } {
|
|
73
|
+
const changes: string[] = [];
|
|
74
|
+
|
|
75
|
+
const proxyConfig =
|
|
76
|
+
`\tserver: {\n` +
|
|
77
|
+
`\t\tproxy: {\n` +
|
|
78
|
+
`\t\t\t'/api': {\n` +
|
|
79
|
+
`\t\t\t\ttarget: 'http://localhost:${API_PORT}',\n` +
|
|
80
|
+
`\t\t\t\tchangeOrigin: true,\n` +
|
|
81
|
+
`\t\t\t},\n` +
|
|
82
|
+
`\t\t},\n` +
|
|
83
|
+
`\t},`;
|
|
84
|
+
|
|
85
|
+
const sf = ts.createSourceFile('vite.config.ts', source, ts.ScriptTarget.ESNext, true);
|
|
86
|
+
|
|
87
|
+
// Find the defineConfig() call's object literal argument
|
|
88
|
+
let configObjectEnd = -1;
|
|
89
|
+
|
|
90
|
+
function visit(node: ts.Node) {
|
|
91
|
+
if (configObjectEnd >= 0) return;
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
ts.isCallExpression(node) &&
|
|
95
|
+
ts.isIdentifier(node.expression) &&
|
|
96
|
+
node.expression.text === 'defineConfig'
|
|
97
|
+
) {
|
|
98
|
+
const arg = node.arguments[0];
|
|
99
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
100
|
+
// Insert before the closing brace of the config object
|
|
101
|
+
configObjectEnd = arg.getEnd() - 1; // Position of `}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ts.forEachChild(node, visit);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
visit(sf);
|
|
109
|
+
|
|
110
|
+
if (configObjectEnd >= 0) {
|
|
111
|
+
// Insert the proxy config before the closing `}` of defineConfig({...})
|
|
112
|
+
const before = source.substring(0, configObjectEnd);
|
|
113
|
+
const after = source.substring(configObjectEnd);
|
|
114
|
+
|
|
115
|
+
// Check if there's already content (need a comma)
|
|
116
|
+
const trimmedBefore = before.trimEnd();
|
|
117
|
+
const needsComma = trimmedBefore.endsWith(',') || trimmedBefore.endsWith('{') ? '' : ',';
|
|
118
|
+
|
|
119
|
+
const result = `${before}${needsComma}\n${proxyConfig}\n${after}`;
|
|
120
|
+
changes.push(`Added server.proxy config: /api → http://localhost:${API_PORT}`);
|
|
121
|
+
|
|
122
|
+
return { source: result, changes };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fallback: couldn't find defineConfig — append a comment
|
|
126
|
+
const fallback =
|
|
127
|
+
source +
|
|
128
|
+
'\n\n// TODO: Add Vite proxy for dev mode:\n' +
|
|
129
|
+
'// server: {\n' +
|
|
130
|
+
'// proxy: {\n' +
|
|
131
|
+
`// '/api': { target: 'http://localhost:${API_PORT}', changeOrigin: true },\n` +
|
|
132
|
+
'// },\n' +
|
|
133
|
+
'// },\n';
|
|
134
|
+
|
|
135
|
+
changes.push('Could not auto-patch vite.config.ts — added proxy config as comment');
|
|
136
|
+
return { source: fallback, changes };
|
|
137
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform: app.ts → src/index.ts
|
|
3
|
+
*
|
|
4
|
+
* Converts a v2 createApp()-based entry point into a plain Hono application
|
|
5
|
+
* with agentuity() middleware.
|
|
6
|
+
*
|
|
7
|
+
* Strategy: rather than doing complex AST surgery on the user's file, we
|
|
8
|
+
* generate a clean src/index.ts from the detection data and leave the old
|
|
9
|
+
* app.ts intact (caller deletes it after confirming). This avoids edge cases
|
|
10
|
+
* with custom formatting and gives users a clean starting point.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { V3DetectionResult } from '../../detect-v3';
|
|
14
|
+
|
|
15
|
+
export interface EntryPointTransformResult {
|
|
16
|
+
/** Generated source for src/index.ts, or null if skipped */
|
|
17
|
+
source: string | null;
|
|
18
|
+
/** What was changed */
|
|
19
|
+
changes: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a new src/index.ts from the detection result.
|
|
24
|
+
*/
|
|
25
|
+
export function generateEntryPoint(detection: V3DetectionResult): EntryPointTransformResult {
|
|
26
|
+
if (!detection.hasCreateApp) {
|
|
27
|
+
return { source: null, changes: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const changes: string[] = [];
|
|
31
|
+
const imports: string[] = [];
|
|
32
|
+
const middlewares: string[] = [];
|
|
33
|
+
const routes: string[] = [];
|
|
34
|
+
|
|
35
|
+
// Core imports
|
|
36
|
+
imports.push("import { Hono } from 'hono';");
|
|
37
|
+
imports.push("import { agentuity } from '@agentuity/hono';");
|
|
38
|
+
|
|
39
|
+
// Always add agentuity middleware
|
|
40
|
+
middlewares.push("app.use('*', agentuity());");
|
|
41
|
+
changes.push('Added agentuity() middleware (telemetry + services)');
|
|
42
|
+
|
|
43
|
+
// CORS — if the user had cors config in createApp()
|
|
44
|
+
if (detection.createAppProps.includes('cors')) {
|
|
45
|
+
imports.push("import { cors } from 'hono/cors';");
|
|
46
|
+
middlewares.push(
|
|
47
|
+
'// TODO: Configure CORS options — the v2 sameOrigin option is not available in hono/cors.\n' +
|
|
48
|
+
'// See: https://hono.dev/docs/middleware/builtin/cors\n' +
|
|
49
|
+
"app.use('*', cors());"
|
|
50
|
+
);
|
|
51
|
+
changes.push('Added cors() middleware (review config — v2 sameOrigin not available)');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Router mount — check if there was a router in createApp
|
|
55
|
+
if (detection.createAppProps.includes('router')) {
|
|
56
|
+
// Try to figure out the router path from the original app.ts
|
|
57
|
+
// Default to /api which is the v2 convention
|
|
58
|
+
imports.push("import router from './api';");
|
|
59
|
+
routes.push("app.route('/api', router);");
|
|
60
|
+
changes.push('Mounted API router at /api');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build the file
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
|
|
66
|
+
lines.push(...imports);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('const app = new Hono();');
|
|
69
|
+
lines.push('');
|
|
70
|
+
|
|
71
|
+
if (middlewares.length > 0) {
|
|
72
|
+
lines.push(...middlewares);
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (routes.length > 0) {
|
|
77
|
+
lines.push(...routes);
|
|
78
|
+
lines.push('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push('export default app;');
|
|
82
|
+
lines.push('');
|
|
83
|
+
|
|
84
|
+
changes.push('Generated src/index.ts with Hono app (replaces app.ts + createApp)');
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
source: lines.join('\n'),
|
|
88
|
+
changes,
|
|
89
|
+
};
|
|
90
|
+
}
|