@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,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 → V3 pattern detection.
|
|
3
|
+
*
|
|
4
|
+
* Analyses a project directory and returns a structured report of every v2
|
|
5
|
+
* artefact that needs to be migrated to v3 (framework-agnostic Hono).
|
|
6
|
+
* No files are modified here.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { join, relative, resolve } from 'node:path';
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Known service names
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const SERVICE_NAMES = [
|
|
15
|
+
'kv',
|
|
16
|
+
'vector',
|
|
17
|
+
'stream',
|
|
18
|
+
'queue',
|
|
19
|
+
'email',
|
|
20
|
+
'task',
|
|
21
|
+
'schedule',
|
|
22
|
+
'sandbox',
|
|
23
|
+
'logger',
|
|
24
|
+
];
|
|
25
|
+
/** Map from service name to package */
|
|
26
|
+
export const SERVICE_PACKAGE_MAP = {
|
|
27
|
+
kv: { pkg: '@agentuity/keyvalue', client: 'KeyValueClient' },
|
|
28
|
+
vector: { pkg: '@agentuity/vector', client: 'VectorClient' },
|
|
29
|
+
stream: { pkg: '@agentuity/stream', client: 'StreamClient' },
|
|
30
|
+
queue: { pkg: '@agentuity/queue', client: 'QueueClient' },
|
|
31
|
+
email: { pkg: '@agentuity/email', client: 'EmailClient' },
|
|
32
|
+
task: { pkg: '@agentuity/task', client: 'TaskClient' },
|
|
33
|
+
schedule: { pkg: '@agentuity/schedule', client: 'ScheduleClient' },
|
|
34
|
+
sandbox: { pkg: '@agentuity/sandbox', client: 'SandboxClient' },
|
|
35
|
+
};
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function rel(projectDir, abs) {
|
|
40
|
+
return relative(projectDir, abs);
|
|
41
|
+
}
|
|
42
|
+
function* walkFiles(dir, exts) {
|
|
43
|
+
if (!existsSync(dir))
|
|
44
|
+
return;
|
|
45
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
+
const full = join(dir, entry.name);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
if (['node_modules', 'dist', '.agentuity', '.git'].includes(entry.name))
|
|
49
|
+
continue;
|
|
50
|
+
yield* walkFiles(full, exts);
|
|
51
|
+
}
|
|
52
|
+
else if (entry.isFile() && exts.some((e) => entry.name.endsWith(e))) {
|
|
53
|
+
yield full;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function parseTs(filePath) {
|
|
58
|
+
const src = await Bun.file(filePath).text();
|
|
59
|
+
return ts.createSourceFile(filePath, src, ts.ScriptTarget.ESNext, true);
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// AST analysis helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/**
|
|
65
|
+
* Get properties passed to createApp() call.
|
|
66
|
+
*/
|
|
67
|
+
function getCreateAppProps(sourceFile) {
|
|
68
|
+
const props = [];
|
|
69
|
+
function visit(node) {
|
|
70
|
+
if (ts.isCallExpression(node) &&
|
|
71
|
+
ts.isIdentifier(node.expression) &&
|
|
72
|
+
node.expression.text === 'createApp') {
|
|
73
|
+
const arg = node.arguments[0];
|
|
74
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
75
|
+
for (const prop of arg.properties) {
|
|
76
|
+
if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
|
|
77
|
+
ts.isIdentifier(prop.name)) {
|
|
78
|
+
props.push(prop.name.text);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ts.forEachChild(node, visit);
|
|
84
|
+
}
|
|
85
|
+
visit(sourceFile);
|
|
86
|
+
return props;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if a source file imports createApp from @agentuity/runtime.
|
|
90
|
+
*/
|
|
91
|
+
function hasCreateAppImport(sourceFile) {
|
|
92
|
+
let found = false;
|
|
93
|
+
function visit(node) {
|
|
94
|
+
if (found)
|
|
95
|
+
return;
|
|
96
|
+
if (ts.isImportDeclaration(node)) {
|
|
97
|
+
const moduleSpecifier = node.moduleSpecifier.text;
|
|
98
|
+
if (moduleSpecifier === '@agentuity/runtime') {
|
|
99
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
100
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
101
|
+
for (const element of namedBindings.elements) {
|
|
102
|
+
if (element.name.text === 'createApp') {
|
|
103
|
+
found = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
ts.forEachChild(node, visit);
|
|
111
|
+
}
|
|
112
|
+
visit(sourceFile);
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Analyse an agent file for complexity classification.
|
|
117
|
+
*/
|
|
118
|
+
function analyseAgentFile(sourceFile) {
|
|
119
|
+
let agentName = null;
|
|
120
|
+
let hasSchema = false;
|
|
121
|
+
let hasSetup = false;
|
|
122
|
+
let hasShutdown = false;
|
|
123
|
+
let hasOnEvent = false;
|
|
124
|
+
let hasModuleLevelCode = false;
|
|
125
|
+
let hasConfigAccess = false;
|
|
126
|
+
let hasAppAccess = false;
|
|
127
|
+
const ctxServices = new Set();
|
|
128
|
+
// Track what's at module level vs inside createAgent
|
|
129
|
+
let insideCreateAgent = false;
|
|
130
|
+
function visitCreateAgentConfig(node) {
|
|
131
|
+
// Inside the config object of createAgent
|
|
132
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
|
|
133
|
+
const propName = node.name.text;
|
|
134
|
+
if (propName === 'schema')
|
|
135
|
+
hasSchema = true;
|
|
136
|
+
if (propName === 'setup')
|
|
137
|
+
hasSetup = true;
|
|
138
|
+
if (propName === 'shutdown')
|
|
139
|
+
hasShutdown = true;
|
|
140
|
+
if (propName === 'on' ||
|
|
141
|
+
propName === 'onStarted' ||
|
|
142
|
+
propName === 'onCompleted' ||
|
|
143
|
+
propName === 'onErrored') {
|
|
144
|
+
hasOnEvent = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Look for ctx.kv, ctx.vector, etc. inside handler
|
|
148
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
149
|
+
const serviceName = node.name.text;
|
|
150
|
+
if (SERVICE_NAMES.includes(serviceName)) {
|
|
151
|
+
// Check if accessing on a parameter-like identifier (ctx, context, c)
|
|
152
|
+
if (ts.isIdentifier(node.expression)) {
|
|
153
|
+
const objName = node.expression.text;
|
|
154
|
+
if (['ctx', 'context', 'c'].includes(objName)) {
|
|
155
|
+
ctxServices.add(serviceName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Check for ctx.config access
|
|
160
|
+
if (node.name.text === 'config' && ts.isIdentifier(node.expression)) {
|
|
161
|
+
if (['ctx', 'context'].includes(node.expression.text)) {
|
|
162
|
+
hasConfigAccess = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Check for ctx.app access
|
|
166
|
+
if (node.name.text === 'app' && ts.isIdentifier(node.expression)) {
|
|
167
|
+
if (['ctx', 'context'].includes(node.expression.text)) {
|
|
168
|
+
hasAppAccess = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
ts.forEachChild(node, visitCreateAgentConfig);
|
|
173
|
+
}
|
|
174
|
+
function visit(node) {
|
|
175
|
+
// Detect createAgent() call
|
|
176
|
+
if (ts.isCallExpression(node) &&
|
|
177
|
+
ts.isIdentifier(node.expression) &&
|
|
178
|
+
node.expression.text === 'createAgent') {
|
|
179
|
+
// First arg is the name
|
|
180
|
+
if (node.arguments[0] && ts.isStringLiteral(node.arguments[0])) {
|
|
181
|
+
agentName = node.arguments[0].text;
|
|
182
|
+
}
|
|
183
|
+
// Second arg is the config
|
|
184
|
+
if (node.arguments[1]) {
|
|
185
|
+
insideCreateAgent = true;
|
|
186
|
+
visitCreateAgentConfig(node.arguments[1]);
|
|
187
|
+
insideCreateAgent = false;
|
|
188
|
+
}
|
|
189
|
+
return; // Don't recurse into createAgent children again
|
|
190
|
+
}
|
|
191
|
+
// Check for module-level statements that aren't just imports/exports/type declarations
|
|
192
|
+
if (!insideCreateAgent && isModuleLevelCode(node, sourceFile)) {
|
|
193
|
+
hasModuleLevelCode = true;
|
|
194
|
+
}
|
|
195
|
+
ts.forEachChild(node, visit);
|
|
196
|
+
}
|
|
197
|
+
visit(sourceFile);
|
|
198
|
+
// Classify complexity
|
|
199
|
+
const complexityReasons = [];
|
|
200
|
+
if (hasSetup)
|
|
201
|
+
complexityReasons.push('has setup() lifecycle hook');
|
|
202
|
+
if (hasShutdown)
|
|
203
|
+
complexityReasons.push('has shutdown() lifecycle hook');
|
|
204
|
+
if (hasOnEvent)
|
|
205
|
+
complexityReasons.push('has event listeners');
|
|
206
|
+
if (hasConfigAccess)
|
|
207
|
+
complexityReasons.push('accesses ctx.config (from setup)');
|
|
208
|
+
if (hasAppAccess)
|
|
209
|
+
complexityReasons.push('accesses ctx.app (app state)');
|
|
210
|
+
if (hasModuleLevelCode)
|
|
211
|
+
complexityReasons.push('has module-level code beyond imports/exports');
|
|
212
|
+
const complexity = complexityReasons.length > 0 ? 'complex' : 'simple';
|
|
213
|
+
return {
|
|
214
|
+
name: agentName,
|
|
215
|
+
complexity,
|
|
216
|
+
complexityReason: complexityReasons.length > 0 ? complexityReasons.join('; ') : undefined,
|
|
217
|
+
hasSchema,
|
|
218
|
+
ctxServices: [...ctxServices],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Check if a node is "module-level code" (not an import, export, type, or interface).
|
|
223
|
+
*/
|
|
224
|
+
function isModuleLevelCode(node, sourceFile) {
|
|
225
|
+
// Only check top-level statements
|
|
226
|
+
if (node.parent !== sourceFile)
|
|
227
|
+
return false;
|
|
228
|
+
// End-of-file token is not code
|
|
229
|
+
if (node.kind === ts.SyntaxKind.EndOfFileToken)
|
|
230
|
+
return false;
|
|
231
|
+
// Imports are fine
|
|
232
|
+
if (ts.isImportDeclaration(node))
|
|
233
|
+
return false;
|
|
234
|
+
// Type-only declarations are fine
|
|
235
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
236
|
+
return false;
|
|
237
|
+
if (ts.isInterfaceDeclaration(node))
|
|
238
|
+
return false;
|
|
239
|
+
// Export default createAgent(...) is fine
|
|
240
|
+
if (ts.isExportAssignment(node))
|
|
241
|
+
return false;
|
|
242
|
+
// Export declarations (re-exports) are fine
|
|
243
|
+
if (ts.isExportDeclaration(node))
|
|
244
|
+
return false;
|
|
245
|
+
// Variable statements
|
|
246
|
+
if (ts.isVariableStatement(node)) {
|
|
247
|
+
// `const` declarations are fine — they're just data, schemas, or config.
|
|
248
|
+
// They'll be preserved alongside the extracted function.
|
|
249
|
+
if (node.declarationList.flags & ts.NodeFlags.Const ||
|
|
250
|
+
node.declarationList.flags & ts.NodeFlags.Using) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
// `let`/`var` with createAgent() is fine
|
|
254
|
+
const declarations = node.declarationList.declarations;
|
|
255
|
+
if (declarations.length === 1) {
|
|
256
|
+
const decl = declarations[0];
|
|
257
|
+
const init = decl?.initializer;
|
|
258
|
+
if (init &&
|
|
259
|
+
ts.isCallExpression(init) &&
|
|
260
|
+
ts.isIdentifier(init.expression) &&
|
|
261
|
+
init.expression.text === 'createAgent') {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (init &&
|
|
265
|
+
ts.isAwaitExpression(init) &&
|
|
266
|
+
ts.isCallExpression(init.expression) &&
|
|
267
|
+
ts.isIdentifier(init.expression.expression) &&
|
|
268
|
+
init.expression.expression.text === 'createAgent') {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// `let`/`var` with mutable state → complex
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
// Enum declarations are fine
|
|
276
|
+
if (ts.isEnumDeclaration(node))
|
|
277
|
+
return false;
|
|
278
|
+
// Function declarations are fine — they're just helper functions
|
|
279
|
+
// that will be preserved alongside the extracted agent function.
|
|
280
|
+
if (ts.isFunctionDeclaration(node))
|
|
281
|
+
return false;
|
|
282
|
+
// Expression statements that are just createAgent() exports are fine
|
|
283
|
+
if (ts.isExpressionStatement(node)) {
|
|
284
|
+
const expr = node.expression;
|
|
285
|
+
if (ts.isCallExpression(expr) &&
|
|
286
|
+
ts.isIdentifier(expr.expression) &&
|
|
287
|
+
expr.expression.text === 'createAgent') {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Everything else is module-level code that indicates complexity
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Scan a source file for service usage patterns: c.var.kv, c.var.vector, etc.
|
|
296
|
+
*/
|
|
297
|
+
function scanServiceUsageInRouteFile(sourceFile) {
|
|
298
|
+
const services = new Set();
|
|
299
|
+
function visit(node) {
|
|
300
|
+
// Match c.var.kv, c.var.vector, etc.
|
|
301
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
302
|
+
ts.isIdentifier(node.name) &&
|
|
303
|
+
SERVICE_NAMES.includes(node.name.text)) {
|
|
304
|
+
// Check it's *.var.service
|
|
305
|
+
if (ts.isPropertyAccessExpression(node.expression) &&
|
|
306
|
+
ts.isIdentifier(node.expression.name) &&
|
|
307
|
+
node.expression.name.text === 'var') {
|
|
308
|
+
services.add(node.name.text);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
ts.forEachChild(node, visit);
|
|
312
|
+
}
|
|
313
|
+
visit(sourceFile);
|
|
314
|
+
return [...services];
|
|
315
|
+
}
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Main detector
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
export async function detectV3(projectDir) {
|
|
320
|
+
const absDir = resolve(projectDir);
|
|
321
|
+
const findings = [];
|
|
322
|
+
const result = {
|
|
323
|
+
projectDir: absDir,
|
|
324
|
+
findings,
|
|
325
|
+
hasRuntimeDep: false,
|
|
326
|
+
hasCreateApp: false,
|
|
327
|
+
createAppProps: [],
|
|
328
|
+
agentFiles: [],
|
|
329
|
+
hasAgentBarrel: false,
|
|
330
|
+
serviceUsages: [],
|
|
331
|
+
allServicesUsed: [],
|
|
332
|
+
hasFrontend: false,
|
|
333
|
+
hasAgentuityConfig: false,
|
|
334
|
+
hasViteConfig: false,
|
|
335
|
+
outdatedPackages: [],
|
|
336
|
+
hasReactPackage: false,
|
|
337
|
+
hasFrontendPackage: false,
|
|
338
|
+
};
|
|
339
|
+
// ── 1. package.json analysis ────────────────────────────────────────────
|
|
340
|
+
const packageJsonPath = join(absDir, 'package.json');
|
|
341
|
+
if (existsSync(packageJsonPath)) {
|
|
342
|
+
try {
|
|
343
|
+
const packageJson = JSON.parse(await Bun.file(packageJsonPath).text());
|
|
344
|
+
for (const section of ['dependencies', 'devDependencies']) {
|
|
345
|
+
const deps = packageJson[section];
|
|
346
|
+
if (!deps || typeof deps !== 'object')
|
|
347
|
+
continue;
|
|
348
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
349
|
+
if (name === '@agentuity/runtime') {
|
|
350
|
+
result.hasRuntimeDep = true;
|
|
351
|
+
result.runtimeVersion = String(version);
|
|
352
|
+
}
|
|
353
|
+
if (name === '@agentuity/react') {
|
|
354
|
+
result.hasReactPackage = true;
|
|
355
|
+
}
|
|
356
|
+
if (name === '@agentuity/frontend') {
|
|
357
|
+
result.hasFrontendPackage = true;
|
|
358
|
+
}
|
|
359
|
+
// Check for outdated packages
|
|
360
|
+
if (name.startsWith('@agentuity/')) {
|
|
361
|
+
const versionStr = String(version);
|
|
362
|
+
const needsUpdate = versionStr === 'latest' ||
|
|
363
|
+
versionStr === '*' ||
|
|
364
|
+
versionStr.startsWith('workspace:') ||
|
|
365
|
+
/^[~^]?[12]\./.test(versionStr);
|
|
366
|
+
if (needsUpdate) {
|
|
367
|
+
result.outdatedPackages.push({
|
|
368
|
+
name,
|
|
369
|
+
currentVersion: versionStr,
|
|
370
|
+
section,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Ignore parse errors
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (result.hasRuntimeDep) {
|
|
382
|
+
findings.push({
|
|
383
|
+
id: 'v3-runtime-dep',
|
|
384
|
+
severity: 'auto',
|
|
385
|
+
message: `@agentuity/runtime@${result.runtimeVersion} found — will be removed`,
|
|
386
|
+
file: 'package.json',
|
|
387
|
+
hint: 'v3 is framework-agnostic. @agentuity/runtime is replaced by:\n' +
|
|
388
|
+
' • hono — the web framework\n' +
|
|
389
|
+
' • @agentuity/hono — middleware for telemetry + services\n' +
|
|
390
|
+
' • Individual service packages (@agentuity/keyvalue, etc.)',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (result.outdatedPackages.length > 0) {
|
|
394
|
+
const packageList = result.outdatedPackages
|
|
395
|
+
.map((p) => `${p.name}@${p.currentVersion}`)
|
|
396
|
+
.join(', ');
|
|
397
|
+
findings.push({
|
|
398
|
+
id: 'v3-outdated-packages',
|
|
399
|
+
severity: 'auto',
|
|
400
|
+
message: `Outdated @agentuity/* packages: ${packageList}`,
|
|
401
|
+
file: 'package.json',
|
|
402
|
+
hint: 'All @agentuity/* packages will be updated to their v3 versions.',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
// ── 2. app.ts / entry point ─────────────────────────────────────────────
|
|
406
|
+
const appTsPath = join(absDir, 'app.ts');
|
|
407
|
+
if (existsSync(appTsPath)) {
|
|
408
|
+
result.appTsPath = appTsPath;
|
|
409
|
+
const sourceFile = await parseTs(appTsPath);
|
|
410
|
+
result.hasCreateApp = hasCreateAppImport(sourceFile);
|
|
411
|
+
if (result.hasCreateApp) {
|
|
412
|
+
result.createAppProps = getCreateAppProps(sourceFile);
|
|
413
|
+
findings.push({
|
|
414
|
+
id: 'v3-createapp',
|
|
415
|
+
severity: 'auto',
|
|
416
|
+
message: 'app.ts uses createApp() from @agentuity/runtime',
|
|
417
|
+
file: 'app.ts',
|
|
418
|
+
hint: 'Will be rewritten to a plain Hono app at src/index.ts:\n' +
|
|
419
|
+
'\n' +
|
|
420
|
+
" import { Hono } from 'hono';\n" +
|
|
421
|
+
" import { agentuity } from '@agentuity/hono';\n" +
|
|
422
|
+
'\n' +
|
|
423
|
+
' const app = new Hono();\n' +
|
|
424
|
+
" app.use('*', agentuity());\n" +
|
|
425
|
+
'\n' +
|
|
426
|
+
' export default app;',
|
|
427
|
+
});
|
|
428
|
+
// Check for cors config
|
|
429
|
+
if (result.createAppProps.includes('cors')) {
|
|
430
|
+
findings.push({
|
|
431
|
+
id: 'v3-cors-config',
|
|
432
|
+
severity: 'guided',
|
|
433
|
+
message: 'createApp() has cors configuration',
|
|
434
|
+
file: 'app.ts',
|
|
435
|
+
hint: 'CORS config will be migrated to hono/cors middleware.\n' +
|
|
436
|
+
"Note: Agentuity-specific options like 'sameOrigin' are not available\n" +
|
|
437
|
+
'in hono/cors. You may need to configure allowedOrigins manually.',
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
// Check for agents reference
|
|
441
|
+
if (result.createAppProps.includes('agents')) {
|
|
442
|
+
findings.push({
|
|
443
|
+
id: 'v3-agents-in-createapp',
|
|
444
|
+
severity: 'auto',
|
|
445
|
+
message: 'createApp() passes agents array — concept removed in v3',
|
|
446
|
+
file: 'app.ts',
|
|
447
|
+
hint: 'Agents are converted to plain functions. The agents import will be removed.',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Also scan app.ts for service usage
|
|
452
|
+
const appServices = scanServiceUsageInRouteFile(sourceFile);
|
|
453
|
+
if (appServices.length > 0) {
|
|
454
|
+
result.serviceUsages.push({
|
|
455
|
+
path: appTsPath,
|
|
456
|
+
relativePath: 'app.ts',
|
|
457
|
+
services: appServices,
|
|
458
|
+
accessPattern: 'c.var',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// ── 3. Agent files ──────────────────────────────────────────────────────
|
|
463
|
+
const agentDir = join(absDir, 'src', 'agent');
|
|
464
|
+
if (existsSync(agentDir)) {
|
|
465
|
+
for (const file of walkFiles(agentDir, ['.ts', '.tsx'])) {
|
|
466
|
+
const base = file.split('/').pop() ?? '';
|
|
467
|
+
// Skip index.ts barrel
|
|
468
|
+
if (base === 'index.ts' || base === 'index.tsx')
|
|
469
|
+
continue;
|
|
470
|
+
const sourceFile = await parseTs(file);
|
|
471
|
+
const src = await Bun.file(file).text();
|
|
472
|
+
// Check if this file uses createAgent
|
|
473
|
+
if (!src.includes('createAgent'))
|
|
474
|
+
continue;
|
|
475
|
+
const analysis = analyseAgentFile(sourceFile);
|
|
476
|
+
if (!analysis.name)
|
|
477
|
+
continue;
|
|
478
|
+
const relPath = rel(absDir, file);
|
|
479
|
+
const agentFile = {
|
|
480
|
+
path: file,
|
|
481
|
+
relativePath: relPath,
|
|
482
|
+
name: analysis.name,
|
|
483
|
+
complexity: analysis.complexity,
|
|
484
|
+
complexityReason: analysis.complexityReason,
|
|
485
|
+
hasSchema: analysis.hasSchema,
|
|
486
|
+
ctxServices: analysis.ctxServices,
|
|
487
|
+
};
|
|
488
|
+
result.agentFiles.push(agentFile);
|
|
489
|
+
if (analysis.complexity === 'simple') {
|
|
490
|
+
findings.push({
|
|
491
|
+
id: `v3-agent-simple:${relPath}`,
|
|
492
|
+
severity: 'auto',
|
|
493
|
+
message: `Agent "${analysis.name}" is simple — will be converted to plain function`,
|
|
494
|
+
file: relPath,
|
|
495
|
+
hint: 'The createAgent() wrapper will be removed. The handler becomes a plain\n' +
|
|
496
|
+
'exported async function.' +
|
|
497
|
+
(analysis.hasSchema
|
|
498
|
+
? ' Schema validation will be preserved in the function.'
|
|
499
|
+
: ''),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
findings.push({
|
|
504
|
+
id: `v3-agent-complex:${relPath}`,
|
|
505
|
+
severity: 'manual',
|
|
506
|
+
message: `Agent "${analysis.name}" is complex — requires manual migration`,
|
|
507
|
+
file: relPath,
|
|
508
|
+
hint: `Complexity: ${analysis.complexityReason}\n` +
|
|
509
|
+
'\n' +
|
|
510
|
+
'This agent uses features beyond a simple handler and cannot be\n' +
|
|
511
|
+
'automatically converted. You need to:\n' +
|
|
512
|
+
' 1. Extract the handler into a plain async function\n' +
|
|
513
|
+
' 2. Move setup logic to module-level initialization\n' +
|
|
514
|
+
' 3. Replace ctx.config/ctx.app with direct imports\n' +
|
|
515
|
+
' 4. Remove event listeners (use your own event patterns)',
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// Track service usage from agents
|
|
519
|
+
if (analysis.ctxServices.length > 0) {
|
|
520
|
+
result.serviceUsages.push({
|
|
521
|
+
path: file,
|
|
522
|
+
relativePath: relPath,
|
|
523
|
+
services: analysis.ctxServices,
|
|
524
|
+
accessPattern: 'ctx',
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Agent barrel
|
|
530
|
+
const agentBarrelPath = join(absDir, 'src', 'agent', 'index.ts');
|
|
531
|
+
result.hasAgentBarrel = existsSync(agentBarrelPath);
|
|
532
|
+
if (result.hasAgentBarrel) {
|
|
533
|
+
findings.push({
|
|
534
|
+
id: 'v3-agent-barrel',
|
|
535
|
+
severity: 'auto',
|
|
536
|
+
message: 'src/agent/index.ts barrel — will be removed',
|
|
537
|
+
file: 'src/agent/index.ts',
|
|
538
|
+
hint: 'The agents barrel exported an array of agents for createApp().\n' +
|
|
539
|
+
'In v3, there is no agents concept — functions are imported directly\n' +
|
|
540
|
+
'where needed.',
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
// ── 4. Route files — service usage ──────────────────────────────────────
|
|
544
|
+
const apiDir = join(absDir, 'src', 'api');
|
|
545
|
+
if (existsSync(apiDir)) {
|
|
546
|
+
for (const file of walkFiles(apiDir, ['.ts', '.tsx'])) {
|
|
547
|
+
const sourceFile = await parseTs(file);
|
|
548
|
+
const services = scanServiceUsageInRouteFile(sourceFile);
|
|
549
|
+
if (services.length > 0) {
|
|
550
|
+
const relPath = rel(absDir, file);
|
|
551
|
+
result.serviceUsages.push({
|
|
552
|
+
path: file,
|
|
553
|
+
relativePath: relPath,
|
|
554
|
+
services,
|
|
555
|
+
accessPattern: 'c.var',
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Also scan src/ root-level TS files
|
|
561
|
+
const srcDir = join(absDir, 'src');
|
|
562
|
+
if (existsSync(srcDir)) {
|
|
563
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
564
|
+
if (entry.isFile() &&
|
|
565
|
+
(entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) &&
|
|
566
|
+
entry.name !== 'services.ts' // Don't scan our own generated file
|
|
567
|
+
) {
|
|
568
|
+
const file = join(srcDir, entry.name);
|
|
569
|
+
const sourceFile = await parseTs(file);
|
|
570
|
+
const services = scanServiceUsageInRouteFile(sourceFile);
|
|
571
|
+
if (services.length > 0) {
|
|
572
|
+
const relPath = rel(absDir, file);
|
|
573
|
+
result.serviceUsages.push({
|
|
574
|
+
path: file,
|
|
575
|
+
relativePath: relPath,
|
|
576
|
+
services,
|
|
577
|
+
accessPattern: 'c.var',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Compute all services used
|
|
584
|
+
const allServices = new Set();
|
|
585
|
+
for (const usage of result.serviceUsages) {
|
|
586
|
+
for (const svc of usage.services) {
|
|
587
|
+
allServices.add(svc);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
result.allServicesUsed = [...allServices].sort();
|
|
591
|
+
if (result.serviceUsages.length > 0) {
|
|
592
|
+
const ctxUsages = result.serviceUsages.filter((u) => u.accessPattern === 'ctx');
|
|
593
|
+
const cVarUsages = result.serviceUsages.filter((u) => u.accessPattern === 'c.var');
|
|
594
|
+
if (ctxUsages.length > 0) {
|
|
595
|
+
findings.push({
|
|
596
|
+
id: 'v3-service-agent-ctx',
|
|
597
|
+
severity: 'guided',
|
|
598
|
+
message: `${ctxUsages.length} file(s) access services via ctx.* (agent context)`,
|
|
599
|
+
hint: 'Service access through agent context (ctx.kv, ctx.vector, etc.) is removed\n' +
|
|
600
|
+
'in v3. A shared src/services.ts will be generated with singleton clients.\n' +
|
|
601
|
+
'\n' +
|
|
602
|
+
" import { kv } from './services'; // or '../services'\n" +
|
|
603
|
+
" const data = await kv.get('namespace', 'key');",
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (cVarUsages.length > 0) {
|
|
607
|
+
findings.push({
|
|
608
|
+
id: 'v3-service-route-ctx',
|
|
609
|
+
severity: 'guided',
|
|
610
|
+
message: `${cVarUsages.length} file(s) access services via c.var.* (Hono context)`,
|
|
611
|
+
hint: 'Service access through Hono context variables (c.var.kv, etc.) is replaced\n' +
|
|
612
|
+
'by direct imports from a shared services module.\n' +
|
|
613
|
+
'\n' +
|
|
614
|
+
" import { kv } from './services';\n" +
|
|
615
|
+
" const data = await kv.get('namespace', 'key');",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// ── 5. Frontend / SPA ───────────────────────────────────────────────────
|
|
620
|
+
const webDir = join(absDir, 'src', 'web');
|
|
621
|
+
result.hasFrontend = existsSync(webDir);
|
|
622
|
+
if (result.hasFrontend) {
|
|
623
|
+
findings.push({
|
|
624
|
+
id: 'v3-spa-detected',
|
|
625
|
+
severity: 'guided',
|
|
626
|
+
message: 'src/web/ frontend detected',
|
|
627
|
+
hint: 'In v3, SPAs are served by the Agentuity buildpack. For local development,\n' +
|
|
628
|
+
"use your framework's dev server (e.g., vite dev). For production, the\n" +
|
|
629
|
+
'buildpack detects static assets and injects a file server automatically.\n' +
|
|
630
|
+
'\n' +
|
|
631
|
+
'If you want to serve static files from your Hono app directly:\n' +
|
|
632
|
+
'\n' +
|
|
633
|
+
" import { serveStatic } from 'hono/bun';\n" +
|
|
634
|
+
" app.use('/assets/*', serveStatic({ root: './src/web/dist' }));",
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
// ── 6. agentuity.config.ts ──────────────────────────────────────────────
|
|
638
|
+
const configPath = join(absDir, 'agentuity.config.ts');
|
|
639
|
+
result.hasAgentuityConfig = existsSync(configPath);
|
|
640
|
+
if (result.hasAgentuityConfig) {
|
|
641
|
+
findings.push({
|
|
642
|
+
id: 'v3-config-file',
|
|
643
|
+
severity: 'auto',
|
|
644
|
+
message: 'agentuity.config.ts exists — will be deleted',
|
|
645
|
+
file: 'agentuity.config.ts',
|
|
646
|
+
hint: 'v3 uses standard framework configuration (vite.config.ts, etc.).\n' +
|
|
647
|
+
'The agentuity.config.ts file is no longer used.',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
// Vite config
|
|
651
|
+
const viteConfigPath = join(absDir, 'vite.config.ts');
|
|
652
|
+
result.hasViteConfig = existsSync(viteConfigPath);
|
|
653
|
+
// ── 7. @agentuity/react deprecation ─────────────────────────────────────
|
|
654
|
+
if (result.hasReactPackage) {
|
|
655
|
+
findings.push({
|
|
656
|
+
id: 'v3-react-deprecated',
|
|
657
|
+
severity: 'manual',
|
|
658
|
+
message: '@agentuity/react is deprecated and will be removed',
|
|
659
|
+
file: 'package.json',
|
|
660
|
+
hint: '@agentuity/react is fully deprecated in v3. Replace with:\n' +
|
|
661
|
+
'\n' +
|
|
662
|
+
' • AgentuityProvider/useAuth → Your auth provider directly (better-auth, Clerk, etc.)\n' +
|
|
663
|
+
' • useAPI/createAPIClient → Hono RPC client (hc from hono/client)\n' +
|
|
664
|
+
' • useAnalytics → getAnalytics() from @agentuity/analytics\n' +
|
|
665
|
+
' • useWebRTCCall → WebRTCManager from @agentuity/frontend\n' +
|
|
666
|
+
'\n' +
|
|
667
|
+
'Remove @agentuity/react from package.json after migrating all imports.',
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
// Sort: auto first, guided second, manual last
|
|
671
|
+
const order = { auto: 0, guided: 1, manual: 2 };
|
|
672
|
+
findings.sort((a, b) => order[a.severity] - order[b.severity]);
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=detect-v3.js.map
|