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