@agentuity/migrate 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +203 -0
  2. package/bin/migrate.ts +60 -0
  3. package/dist/detect.d.ts +56 -0
  4. package/dist/detect.d.ts.map +1 -0
  5. package/dist/detect.js +561 -0
  6. package/dist/detect.js.map +1 -0
  7. package/dist/index.d.ts +9 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +9 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/migrate.d.ts +29 -0
  12. package/dist/migrate.d.ts.map +1 -0
  13. package/dist/migrate.js +315 -0
  14. package/dist/migrate.js.map +1 -0
  15. package/dist/report.d.ts +22 -0
  16. package/dist/report.d.ts.map +1 -0
  17. package/dist/report.js +159 -0
  18. package/dist/report.js.map +1 -0
  19. package/dist/transforms/app-ts.d.ts +29 -0
  20. package/dist/transforms/app-ts.d.ts.map +1 -0
  21. package/dist/transforms/app-ts.js +114 -0
  22. package/dist/transforms/app-ts.js.map +1 -0
  23. package/dist/transforms/barrels.d.ts +12 -0
  24. package/dist/transforms/barrels.d.ts.map +1 -0
  25. package/dist/transforms/barrels.js +103 -0
  26. package/dist/transforms/barrels.js.map +1 -0
  27. package/dist/transforms/generated.d.ts +7 -0
  28. package/dist/transforms/generated.d.ts.map +1 -0
  29. package/dist/transforms/generated.js +10 -0
  30. package/dist/transforms/generated.js.map +1 -0
  31. package/dist/transforms/routes.d.ts +49 -0
  32. package/dist/transforms/routes.d.ts.map +1 -0
  33. package/dist/transforms/routes.js +208 -0
  34. package/dist/transforms/routes.js.map +1 -0
  35. package/package.json +45 -0
  36. package/src/detect.ts +694 -0
  37. package/src/index.ts +9 -0
  38. package/src/migrate.ts +379 -0
  39. package/src/report.ts +195 -0
  40. package/src/transforms/app-ts.ts +144 -0
  41. package/src/transforms/barrels.ts +138 -0
  42. package/src/transforms/generated.ts +11 -0
  43. package/src/transforms/routes.ts +273 -0
package/src/detect.ts ADDED
@@ -0,0 +1,694 @@
1
+ /**
2
+ * V1 pattern detection.
3
+ *
4
+ * Analyses a project directory and returns a structured report of every v1
5
+ * artefact that needs to be migrated to v2. No files are modified here.
6
+ */
7
+
8
+ import { existsSync, readdirSync, statSync } from 'node:fs';
9
+ import { join, relative, resolve } from 'node:path';
10
+ import ts from 'typescript';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Public types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** Severity of a finding — drives the interactive prompt */
17
+ export type Severity =
18
+ | 'auto' // fully mechanical; the codemod will handle it without user input
19
+ | 'guided' // the tool can apply a transform but needs user to verify
20
+ | 'manual'; // requires human attention; tool will explain but not touch
21
+
22
+ export interface Finding {
23
+ /** Short identifier used to reference this finding in transforms */
24
+ id: string;
25
+ severity: Severity;
26
+ /** Human-readable summary */
27
+ message: string;
28
+ /** File relative to project root, or undefined for project-level findings */
29
+ file?: string;
30
+ /** Extra detail or migration hint shown in the report */
31
+ hint?: string;
32
+ }
33
+
34
+ export interface DetectionResult {
35
+ projectDir: string;
36
+ /** All findings, ordered by severity (auto → guided → manual) */
37
+ findings: Finding[];
38
+ /** Absolute path to app.ts, if found */
39
+ appTsPath?: string;
40
+ /** Absolute path to src/generated dir, if it exists */
41
+ generatedDir?: string;
42
+ /** Absolute paths of route files detected as v1-style (mutating createRouter) */
43
+ v1RouteFiles: string[];
44
+ /** Absolute paths of route files already in v2-style (chained Hono) */
45
+ v2RouteFiles: string[];
46
+ /** Whether src/agent/index.ts barrel exists */
47
+ hasAgentBarrel: boolean;
48
+ /** Whether src/api/index.ts barrel exists */
49
+ hasApiBarrel: boolean;
50
+ /** Whether agentuity.config.ts exists */
51
+ hasAgentuityConfig: boolean;
52
+ /** Whether app.ts passes analytics/workbench inside createApp() */
53
+ analyticsInCreateApp: boolean;
54
+ workbenchInCreateApp: boolean;
55
+ /** Whether app.ts passes setup/shutdown inside createApp() */
56
+ setupInCreateApp: boolean;
57
+ shutdownInCreateApp: boolean;
58
+ /** Whether app.ts calls bootstrapRuntimeEnv() */
59
+ bootstrapCallInAppTs: boolean;
60
+ /** Whether frontend code uses removed APIs */
61
+ frontendRemovedApis: FrontendFinding[];
62
+ }
63
+
64
+ export interface FrontendFinding {
65
+ file: string;
66
+ apis: string[];
67
+ /** APIs that are deprecated (still work but should migrate away) */
68
+ deprecatedApis?: string[];
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Removed frontend API names
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const REMOVED_REACT_APIS = new Set([
76
+ 'createAPIClient',
77
+ 'useAPI',
78
+ 'createClient',
79
+ 'RouteRegistry',
80
+ 'RPCRouteRegistry',
81
+ 'SSERouteRegistry',
82
+ 'WebSocketRouteRegistry',
83
+ ]);
84
+
85
+ /**
86
+ * APIs that are deprecated (still work but will be removed).
87
+ * Users should migrate away from these.
88
+ */
89
+ const DEPRECATED_REACT_APIS = new Set([
90
+ 'AgentuityProvider',
91
+ 'AgentuityContext',
92
+ 'useAgentuity',
93
+ 'useAuth',
94
+ 'useAnalytics',
95
+ 'useTrackOnMount',
96
+ 'withPageTracking',
97
+ 'useWebRTCCall',
98
+ 'useJsonMemo',
99
+ ]);
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Helpers
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function rel(projectDir: string, abs: string): string {
106
+ return relative(projectDir, abs);
107
+ }
108
+
109
+ /**
110
+ * Walk a directory recursively, yielding all file paths that match the
111
+ * given extension filter.
112
+ */
113
+ function* walkFiles(dir: string, exts: string[]): Generator<string> {
114
+ if (!existsSync(dir)) return;
115
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
116
+ const full = join(dir, entry.name);
117
+ if (entry.isDirectory()) {
118
+ // Skip known non-source dirs
119
+ if (['node_modules', 'dist', '.agentuity', '.git'].includes(entry.name)) continue;
120
+ yield* walkFiles(full, exts);
121
+ } else if (entry.isFile() && exts.some((e) => entry.name.endsWith(e))) {
122
+ yield full;
123
+ }
124
+ }
125
+ }
126
+
127
+ /** Parse a TypeScript source file and return the AST */
128
+ async function parseTs(filePath: string): Promise<ts.SourceFile> {
129
+ const src = await Bun.file(filePath).text();
130
+ return ts.createSourceFile(filePath, src, ts.ScriptTarget.ESNext, true);
131
+ }
132
+
133
+ /**
134
+ * Collect the property names present inside a specific function call's first
135
+ * object-literal argument.
136
+ *
137
+ * e.g. given `createApp({ router, agents, setup })` → ['router','agents','setup']
138
+ */
139
+ function getCreateAppProps(sourceFile: ts.SourceFile): Set<string> {
140
+ const props = new Set<string>();
141
+
142
+ function visit(node: ts.Node) {
143
+ if (
144
+ ts.isCallExpression(node) &&
145
+ ts.isIdentifier(node.expression) &&
146
+ node.expression.text === 'createApp'
147
+ ) {
148
+ const arg = node.arguments[0];
149
+ if (arg && ts.isObjectLiteralExpression(arg)) {
150
+ for (const prop of arg.properties) {
151
+ if (
152
+ (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
153
+ ts.isIdentifier(prop.name)
154
+ ) {
155
+ props.add(prop.name.text);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ts.forEachChild(node, visit);
161
+ }
162
+
163
+ visit(sourceFile);
164
+ return props;
165
+ }
166
+
167
+ /**
168
+ * Returns true if the source file contains a call to `bootstrapRuntimeEnv()`
169
+ */
170
+ function hasBootstrapCall(sourceFile: ts.SourceFile): boolean {
171
+ let found = false;
172
+
173
+ function visit(node: ts.Node) {
174
+ if (found) return;
175
+ if (
176
+ ts.isCallExpression(node) &&
177
+ ts.isIdentifier(node.expression) &&
178
+ node.expression.text === 'bootstrapRuntimeEnv'
179
+ ) {
180
+ found = true;
181
+ return;
182
+ }
183
+ ts.forEachChild(node, visit);
184
+ }
185
+
186
+ visit(sourceFile);
187
+ return found;
188
+ }
189
+
190
+ /**
191
+ * Detect whether a route file uses v1 mutable-style `createRouter()` +
192
+ * `router.get(...)` vs v2 chained `new Hono<Env>().get(...)`.
193
+ *
194
+ * Returns 'v1' | 'v2' | 'unknown' (file doesn't look like a route at all).
195
+ */
196
+ function classifyRouteFile(sourceFile: ts.SourceFile): 'v1' | 'v2' | 'unknown' {
197
+ // sync — receives parsed AST
198
+ let hasCreateRouter = false;
199
+ let hasMutatingCall = false; // router.get / router.post etc.
200
+ let hasHonoNew = false;
201
+
202
+ function visit(node: ts.Node) {
203
+ // createRouter() call
204
+ if (
205
+ ts.isCallExpression(node) &&
206
+ ts.isIdentifier(node.expression) &&
207
+ node.expression.text === 'createRouter'
208
+ ) {
209
+ hasCreateRouter = true;
210
+ }
211
+
212
+ // router.<method>(...) — mutating style
213
+ if (
214
+ ts.isCallExpression(node) &&
215
+ ts.isPropertyAccessExpression(node.expression) &&
216
+ ts.isIdentifier(node.expression.name)
217
+ ) {
218
+ const methodName = node.expression.name.text;
219
+ if (
220
+ ['get', 'post', 'put', 'patch', 'delete', 'all', 'use', 'route'].includes(methodName)
221
+ ) {
222
+ // Check that the object being accessed is NOT a chained call (i.e., it's an identifier)
223
+ if (ts.isIdentifier(node.expression.expression)) {
224
+ hasMutatingCall = true;
225
+ }
226
+ }
227
+ }
228
+
229
+ // new Hono<...>()
230
+ if (
231
+ ts.isNewExpression(node) &&
232
+ ts.isIdentifier(node.expression) &&
233
+ node.expression.text === 'Hono'
234
+ ) {
235
+ hasHonoNew = true;
236
+ }
237
+
238
+ ts.forEachChild(node, visit);
239
+ }
240
+
241
+ visit(sourceFile);
242
+
243
+ if (hasHonoNew) return 'v2';
244
+ if (hasCreateRouter && hasMutatingCall) return 'v1';
245
+ return 'unknown';
246
+ }
247
+
248
+ /**
249
+ * Scan frontend (web) source files for removed API usages.
250
+ */
251
+ async function scanFrontendFiles(projectDir: string): Promise<FrontendFinding[]> {
252
+ const webDir = join(projectDir, 'src', 'web');
253
+ const findings: FrontendFinding[] = [];
254
+
255
+ for (const file of walkFiles(webDir, ['.ts', '.tsx'])) {
256
+ const sourceFile = await parseTs(file);
257
+ const usedApis: string[] = [];
258
+ const deprecatedApis: string[] = [];
259
+
260
+ function visit(node: ts.Node) {
261
+ // Check named imports from @agentuity/react or @agentuity/frontend
262
+ if (ts.isImportDeclaration(node)) {
263
+ const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
264
+ if (
265
+ moduleSpecifier === '@agentuity/react' ||
266
+ moduleSpecifier === '@agentuity/frontend'
267
+ ) {
268
+ const namedBindings = node.importClause?.namedBindings;
269
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
270
+ for (const element of namedBindings.elements) {
271
+ const name = element.name.text;
272
+ if (REMOVED_REACT_APIS.has(name)) {
273
+ usedApis.push(name);
274
+ }
275
+ if (DEPRECATED_REACT_APIS.has(name)) {
276
+ deprecatedApis.push(name);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ ts.forEachChild(node, visit);
283
+ }
284
+
285
+ visit(sourceFile);
286
+
287
+ if (usedApis.length > 0 || deprecatedApis.length > 0) {
288
+ findings.push({
289
+ file: rel(projectDir, file),
290
+ apis: [...new Set(usedApis)],
291
+ deprecatedApis: [...new Set(deprecatedApis)],
292
+ });
293
+ }
294
+ }
295
+
296
+ return findings;
297
+ }
298
+
299
+ /**
300
+ * Find all route files under src/api (recursively, files named `route.ts` or
301
+ * `route.tsx` or `router.ts`).
302
+ */
303
+ function findRouteFiles(projectDir: string): string[] {
304
+ const apiDir = join(projectDir, 'src', 'api');
305
+ const files: string[] = [];
306
+
307
+ for (const file of walkFiles(apiDir, ['.ts', '.tsx'])) {
308
+ const base = file.split('/').pop() ?? '';
309
+ if (base === 'route.ts' || base === 'route.tsx' || base === 'router.ts') {
310
+ files.push(file);
311
+ }
312
+ }
313
+
314
+ return files;
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Main detector
319
+ // ---------------------------------------------------------------------------
320
+
321
+ export async function detect(projectDir: string): Promise<DetectionResult> {
322
+ const absDir = resolve(projectDir);
323
+ const findings: Finding[] = [];
324
+
325
+ const result: DetectionResult = {
326
+ projectDir: absDir,
327
+ findings,
328
+ v1RouteFiles: [],
329
+ v2RouteFiles: [],
330
+ hasAgentBarrel: false,
331
+ hasApiBarrel: false,
332
+ hasAgentuityConfig: false,
333
+ analyticsInCreateApp: false,
334
+ workbenchInCreateApp: false,
335
+ setupInCreateApp: false,
336
+ shutdownInCreateApp: false,
337
+ bootstrapCallInAppTs: false,
338
+ frontendRemovedApis: [],
339
+ };
340
+
341
+ // ── 1. src/generated/ ───────────────────────────────────────────────────
342
+ const generatedDir = join(absDir, 'src', 'generated');
343
+ if (existsSync(generatedDir) && statSync(generatedDir).isDirectory()) {
344
+ result.generatedDir = generatedDir;
345
+ findings.push({
346
+ id: 'generated-dir',
347
+ severity: 'auto',
348
+ message: 'src/generated/ directory exists (CLI-managed codegen artefacts)',
349
+ file: rel(absDir, generatedDir),
350
+ hint: 'Will be deleted. In v2, types come from Hono RPC inference and direct imports.',
351
+ });
352
+ }
353
+
354
+ // ── 2. app.ts ────────────────────────────────────────────────────────────
355
+ const appTsPath = join(absDir, 'app.ts');
356
+ if (existsSync(appTsPath)) {
357
+ result.appTsPath = appTsPath;
358
+ const sourceFile = await parseTs(appTsPath);
359
+ const props = getCreateAppProps(sourceFile);
360
+
361
+ result.bootstrapCallInAppTs = hasBootstrapCall(sourceFile);
362
+ if (result.bootstrapCallInAppTs) {
363
+ findings.push({
364
+ id: 'bootstrap-call',
365
+ severity: 'auto',
366
+ message: 'app.ts calls bootstrapRuntimeEnv() — removed in v2',
367
+ file: 'app.ts',
368
+ hint: 'createApp() handles runtime bootstrapping internally; the call will be removed.',
369
+ });
370
+ }
371
+
372
+ result.setupInCreateApp = props.has('setup');
373
+ if (result.setupInCreateApp) {
374
+ findings.push({
375
+ id: 'setup-in-createapp',
376
+ severity: 'guided',
377
+ message: 'createApp({ setup }) — setup() lifecycle removed in v2',
378
+ file: 'app.ts',
379
+ hint:
380
+ 'Move initialisation logic to module-level top-of-file code in app.ts. ' +
381
+ "For cleanup, use Hono's standard patterns or Bun's process hooks (e.g., process.on('beforeExit', ...)).",
382
+ });
383
+ }
384
+
385
+ result.shutdownInCreateApp = props.has('shutdown');
386
+ if (result.shutdownInCreateApp) {
387
+ findings.push({
388
+ id: 'shutdown-in-createapp',
389
+ severity: 'guided',
390
+ message: 'createApp({ shutdown }) — shutdown() lifecycle removed in v2',
391
+ file: 'app.ts',
392
+ hint:
393
+ "Use Hono's standard patterns or Bun's process hooks for cleanup " +
394
+ "(e.g., process.on('beforeExit', async () => { /* cleanup */ })).",
395
+ });
396
+ }
397
+
398
+ result.analyticsInCreateApp = props.has('analytics');
399
+ if (result.analyticsInCreateApp) {
400
+ // v2 keeps analytics in createApp() - no migration needed
401
+ // This is tracked for informational purposes only
402
+ }
403
+
404
+ result.workbenchInCreateApp = props.has('workbench');
405
+ if (result.workbenchInCreateApp) {
406
+ // v2 keeps workbench in createApp() - no migration needed
407
+ // This is tracked for informational purposes only
408
+ }
409
+
410
+ // Check if router/agents are already wired (v2 pattern)
411
+ const hasRouter = props.has('router');
412
+ const hasAgents = props.has('agents');
413
+ if (!hasRouter && !hasAgents) {
414
+ findings.push({
415
+ id: 'app-no-router-agents',
416
+ severity: 'guided',
417
+ message: 'app.ts does not pass router or agents to createApp()',
418
+ file: 'app.ts',
419
+ hint:
420
+ 'In v2, you must explicitly import and pass your router and agents array.\n' +
421
+ '\n' +
422
+ " import router from './src/api';\n" +
423
+ " import agents from './src/agent';\n" +
424
+ '\n' +
425
+ ' const { server, logger } = await createApp({\n' +
426
+ " router: { path: '/api', router },\n" +
427
+ ' agents,\n' +
428
+ ' });\n' +
429
+ '\n' +
430
+ 'The src/api/index.ts and src/agent/index.ts barrels are generated by this tool.',
431
+ });
432
+ }
433
+ } else {
434
+ findings.push({
435
+ id: 'no-app-ts',
436
+ severity: 'manual',
437
+ message: 'No app.ts found at project root',
438
+ hint: 'Create app.ts importing createApp from @agentuity/runtime.',
439
+ });
440
+ }
441
+
442
+ // ── 3. agentuity.config.ts ───────────────────────────────────────────────
443
+ const configPath = join(absDir, 'agentuity.config.ts');
444
+ result.hasAgentuityConfig = existsSync(configPath);
445
+
446
+ // Check for vite.config.ts (v2 approach)
447
+ const viteConfigPath = join(absDir, 'vite.config.ts');
448
+ const hasViteConfig = existsSync(viteConfigPath);
449
+
450
+ // In v2, agentuity.config.ts is DEPRECATED.
451
+ // ALL config is consolidated into createApp() or native Vite config:
452
+ // - Vite keys (plugins, define, render, bundle) → vite.config.ts
453
+ // - Runtime keys (analytics, workbench) → createApp() ONLY (single source of truth)
454
+ if (result.hasAgentuityConfig) {
455
+ const configSrc = await Bun.file(configPath).text();
456
+
457
+ // Categorize config keys
458
+ const viteKeys: string[] = [];
459
+ const runtimeKeys: string[] = [];
460
+
461
+ if (configSrc.includes('plugins:')) viteKeys.push('plugins');
462
+ if (configSrc.includes('define:')) viteKeys.push('define');
463
+ if (configSrc.includes('render:')) viteKeys.push('render');
464
+ if (configSrc.includes('bundle:')) viteKeys.push('bundle');
465
+ if (configSrc.includes('analytics:')) runtimeKeys.push('analytics');
466
+ if (configSrc.includes('workbench:')) runtimeKeys.push('workbench');
467
+
468
+ if (viteKeys.length > 0) {
469
+ findings.push({
470
+ id: 'agentuity-config-vite-keys',
471
+ severity: 'guided',
472
+ message: `agentuity.config.ts has Vite config: ${viteKeys.join(', ')}`,
473
+ file: 'agentuity.config.ts',
474
+ hint:
475
+ 'Vite config should go in vite.config.ts. Example:\n' +
476
+ '\n' +
477
+ ' // vite.config.ts\n' +
478
+ " import { defineConfig } from 'vite';\n" +
479
+ " import react from '@vitejs/plugin-react';\n" +
480
+ '\n' +
481
+ ' export default defineConfig({\n' +
482
+ ' plugins: [react()],\n' +
483
+ " define: { CUSTOM: JSON.stringify('value') },\n" +
484
+ ' });\n' +
485
+ '\n' +
486
+ 'After creating vite.config.ts, delete agentuity.config.ts.',
487
+ });
488
+ }
489
+
490
+ if (runtimeKeys.length > 0) {
491
+ findings.push({
492
+ id: 'agentuity-config-runtime-keys',
493
+ severity: 'guided',
494
+ message: `agentuity.config.ts has ${runtimeKeys.join(', ')} — remove (use createApp() only)`,
495
+ file: 'agentuity.config.ts',
496
+ hint:
497
+ 'In v2, ALL runtime config goes in createApp().\n' +
498
+ 'The CLI reads these options directly from your createApp() call.\n' +
499
+ 'Remove them from agentuity.config.ts and keep only in createApp().',
500
+ });
501
+ }
502
+
503
+ if (viteKeys.length === 0 && runtimeKeys.length === 0) {
504
+ // Empty or minimal config
505
+ findings.push({
506
+ id: 'agentuity-config-empty',
507
+ severity: 'guided',
508
+ message: 'agentuity.config.ts exists but has no config keys — can be deleted',
509
+ file: 'agentuity.config.ts',
510
+ hint: 'This file is no longer needed in v2. Delete it.',
511
+ });
512
+ }
513
+ }
514
+
515
+ // ── 4. Route files ───────────────────────────────────────────────────────
516
+ const routeFiles = findRouteFiles(absDir);
517
+ for (const file of routeFiles) {
518
+ const sourceFile = await parseTs(file);
519
+ const classification = classifyRouteFile(sourceFile);
520
+ const relFile = rel(absDir, file);
521
+
522
+ if (classification === 'v1') {
523
+ result.v1RouteFiles.push(file);
524
+ findings.push({
525
+ id: `route-v1:${relFile}`,
526
+ severity: 'auto',
527
+ message: `Route file uses v1 mutable createRouter() style`,
528
+ file: relFile,
529
+ hint:
530
+ 'Will be rewritten to chained new Hono<Env>().get().post()… style.\n' +
531
+ 'Chaining is required for Hono RPC — only a single chained expression\n' +
532
+ 'accumulates all route schemas into `typeof router`, which becomes the\n' +
533
+ 'AppRouter type used by hc<AppRouter>() on the frontend.',
534
+ });
535
+ } else if (classification === 'v2') {
536
+ result.v2RouteFiles.push(file);
537
+ }
538
+ }
539
+
540
+ // ── 5. src/api/index.ts barrel ──────────────────────────────────────────
541
+ const apiIndexPath = join(absDir, 'src', 'api', 'index.ts');
542
+ result.hasApiBarrel = existsSync(apiIndexPath);
543
+
544
+ if (!result.hasApiBarrel && result.v1RouteFiles.length > 0) {
545
+ findings.push({
546
+ id: 'missing-api-barrel',
547
+ severity: 'auto',
548
+ message: 'src/api/index.ts barrel does not exist',
549
+ hint:
550
+ 'Will be generated with a single chained Hono<Env>().route()… expression.\n' +
551
+ 'This exports AppRouter = typeof router, the type needed for hc<AppRouter>()\n' +
552
+ 'on the frontend. Chaining is required — broken chains produce an empty type.',
553
+ });
554
+ } else if (result.hasApiBarrel) {
555
+ // Check if the existing index.ts is the old empty stub
556
+ const src = await Bun.file(apiIndexPath).text();
557
+ const isStub =
558
+ src.includes('createRouter()') &&
559
+ !src.includes('.route(') &&
560
+ src.split('\n').filter((l) => l.trim()).length < 8;
561
+ if (isStub) {
562
+ findings.push({
563
+ id: 'api-barrel-stub',
564
+ severity: 'auto',
565
+ message: 'src/api/index.ts is the empty v1 stub (createRouter() with no routes)',
566
+ file: 'src/api/index.ts',
567
+ hint: 'Will be replaced with an explicit barrel that imports and composes all route files.',
568
+ });
569
+ }
570
+ }
571
+
572
+ // ── 6. src/agent/index.ts barrel ────────────────────────────────────────
573
+ const agentIndexPath = join(absDir, 'src', 'agent', 'index.ts');
574
+ result.hasAgentBarrel = existsSync(agentIndexPath);
575
+ if (!result.hasAgentBarrel) {
576
+ const agentDir = join(absDir, 'src', 'agent');
577
+ if (existsSync(agentDir)) {
578
+ findings.push({
579
+ id: 'missing-agent-barrel',
580
+ severity: 'auto',
581
+ message: 'src/agent/index.ts barrel does not exist',
582
+ hint: 'Will be generated by scanning src/agent/*/agent.ts files and exporting a default array.',
583
+ });
584
+ }
585
+ }
586
+
587
+ // ── 7. Frontend removed APIs ─────────────────────────────────────────────
588
+ result.frontendRemovedApis = await scanFrontendFiles(absDir);
589
+ for (const fe of result.frontendRemovedApis) {
590
+ if (fe.apis.length > 0) {
591
+ findings.push({
592
+ id: `frontend-removed:${fe.file}`,
593
+ severity: 'manual',
594
+ message: `Frontend file uses removed APIs: ${fe.apis.join(', ')}`,
595
+ file: fe.file,
596
+ hint:
597
+ 'Replace with Hono RPC client (hono/client):\n' +
598
+ '\n' +
599
+ " import { hc } from 'hono/client';\n" +
600
+ " import type { AppRouter } from '../api'; // your barrel\n" +
601
+ " const client = hc<AppRouter>(window.location.origin + '/api');\n" +
602
+ " const res = await client.hello.$post({ json: { name: 'World' } });\n" +
603
+ '\n' +
604
+ 'See: https://hono.dev/docs/guides/rpc',
605
+ });
606
+ }
607
+ if (fe.deprecatedApis && fe.deprecatedApis.length > 0) {
608
+ findings.push({
609
+ id: `frontend-deprecated:${fe.file}`,
610
+ severity: 'guided',
611
+ message: `Frontend file uses deprecated @agentuity/react APIs: ${fe.deprecatedApis.join(', ')}`,
612
+ file: fe.file,
613
+ hint:
614
+ '@agentuity/react is deprecated. Migration options:\n' +
615
+ '\n' +
616
+ '• AgentuityProvider/useAuth → Use your auth provider directly (better-auth, Clerk, etc.)\n' +
617
+ '• useAnalytics → Use getAnalytics() from @agentuity/frontend\n' +
618
+ '• useWebRTCCall → Use WebRTCManager from @agentuity/frontend\n' +
619
+ '• WebSocketManager/EventStreamManager → Import from @agentuity/frontend\n' +
620
+ '\n' +
621
+ 'The package will continue to work but will not receive updates.',
622
+ });
623
+ }
624
+ }
625
+
626
+ // ── 7b. Hono RPC recommendation (whenever routes exist) ─────────────────
627
+ // Fire this as a "guided" finding whenever there are any API routes,
628
+ // regardless of whether the frontend already uses removed APIs.
629
+ // It surfaces the RPC pattern to users who may not know about it yet.
630
+ const hasApiRoutes =
631
+ result.v1RouteFiles.length > 0 ||
632
+ result.v2RouteFiles.length > 0 ||
633
+ existsSync(join(absDir, 'src', 'api'));
634
+
635
+ const frontendDir = join(absDir, 'src', 'web');
636
+ const hasFrontend = existsSync(frontendDir);
637
+
638
+ // ── 7c. Vite config check ───────────────────────────────────────────────
639
+ // If there's a frontend, vite.config.ts should exist with framework plugins
640
+ if (hasFrontend && !hasViteConfig) {
641
+ findings.push({
642
+ id: 'missing-vite-config',
643
+ severity: 'guided',
644
+ message: 'No vite.config.ts found - frontend requires Vite configuration',
645
+ hint:
646
+ 'Create vite.config.ts with your frontend framework plugin:\n' +
647
+ '\n' +
648
+ ' // vite.config.ts\n' +
649
+ " import { defineConfig } from 'vite';\n" +
650
+ " import react from '@vitejs/plugin-react';\n" +
651
+ '\n' +
652
+ ' export default defineConfig({\n' +
653
+ ' plugins: [react()],\n' +
654
+ ' });\n' +
655
+ '\n' +
656
+ 'For other frameworks:\n' +
657
+ " • Svelte: import { svelte } from '@sveltejs/vite-plugin-svelte'\n" +
658
+ " • Vue: import vue from '@vitejs/plugin-vue'\n" +
659
+ " • Solid: import solid from 'vite-plugin-solid'",
660
+ });
661
+ }
662
+
663
+ if (hasApiRoutes && hasFrontend && result.frontendRemovedApis.length === 0) {
664
+ // Only show this if there are no already-detected frontend issues (avoid duplication)
665
+ findings.push({
666
+ id: 'hono-rpc-recommendation',
667
+ severity: 'guided',
668
+ message: 'Consider using Hono RPC for fully type-safe frontend ↔ backend calls',
669
+ hint:
670
+ "v2 uses Hono's native RPC system instead of the old Agentuity typed client.\n" +
671
+ 'The barrel at src/api/index.ts exports AppRouter = typeof router.\n' +
672
+ 'Routes MUST be chained in a single expression for types to accumulate.\n' +
673
+ '\n' +
674
+ 'In your frontend (src/web/):\n' +
675
+ '\n' +
676
+ " import { hc } from 'hono/client';\n" +
677
+ " import type { AppRouter } from '../api';\n" +
678
+ '\n' +
679
+ " const client = hc<AppRouter>(window.location.origin + '/api');\n" +
680
+ '\n' +
681
+ ' // Typed call — method name mirrors route path, prefixed with $\n' +
682
+ " const res = await client.hello.$post({ json: { name: 'World' } });\n" +
683
+ ' const data = await res.json();\n' +
684
+ '\n' +
685
+ 'See: https://hono.dev/docs/guides/rpc',
686
+ });
687
+ }
688
+
689
+ // Sort: auto first, guided second, manual last
690
+ const order: Record<Severity, number> = { auto: 0, guided: 1, manual: 2 };
691
+ findings.sort((a, b) => order[a.severity] - order[b.severity]);
692
+
693
+ return result;
694
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @agentuity/migrate — public API
3
+ *
4
+ * Programmatic access to the migration tool. For CLI usage, use the bin entry:
5
+ * npx @agentuity/migrate [project-dir]
6
+ */
7
+
8
+ export { migrate, type MigrateOptions, type MigrateResult } from './migrate';
9
+ export { detect, type DetectionResult, type Finding, type Severity } from './detect';