@agentuity/migrate 2.0.11 → 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 +10 -5
  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
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * npx @agentuity/migrate [project-dir]
6
6
  */
7
7
 
8
+ // v1 → v2 migration
8
9
  export { migrate, type MigrateOptions, type MigrateResult } from './migrate';
9
10
  export {
10
11
  detect,
@@ -13,3 +14,15 @@ export {
13
14
  type Severity,
14
15
  type OutdatedPackage,
15
16
  } from './detect';
17
+
18
+ // v2 → v3 migration
19
+ export { migrateV3, type MigrateV3Options, type MigrateV3Result } from './migrate-v3';
20
+ export {
21
+ detectV3,
22
+ type V3DetectionResult,
23
+ type V3Finding,
24
+ type V3OutdatedPackage,
25
+ type AgentFile,
26
+ type AgentComplexity,
27
+ type ServiceUsage,
28
+ } from './detect-v3';
@@ -0,0 +1,539 @@
1
+ /**
2
+ * V2 → V3 migration orchestrator.
3
+ *
4
+ * Flow:
5
+ * 1. Check git worktree is clean (bail if not)
6
+ * 2. Run v3 detection
7
+ * 3. Print report
8
+ * 4. Interactive confirmation (unless --yes)
9
+ * 5. Apply transforms:
10
+ * a. Generate src/services.ts
11
+ * b. Transform agent files
12
+ * c. Rewrite service access in route files
13
+ * d. Generate new entry point (src/index.ts)
14
+ * e. Delete old app.ts
15
+ * f. Delete agentuity.config.ts
16
+ * g. Delete src/agent/index.ts barrel
17
+ * h. Update package.json
18
+ * 6. Run bun install
19
+ * 7. Run typecheck
20
+ * 8. Print final summary
21
+ */
22
+
23
+ import { existsSync, writeFileSync, unlinkSync, mkdirSync, readdirSync } from 'node:fs';
24
+ import { join, resolve, dirname } from 'node:path';
25
+
26
+ import { detectV3 } from './detect-v3';
27
+ import {
28
+ printV3Report,
29
+ printStep,
30
+ printStepDone,
31
+ printStepFailed,
32
+ printStepSkipped,
33
+ printWarning,
34
+ printError,
35
+ printSuccess,
36
+ printManualSummaryV3,
37
+ printChangeSummary,
38
+ } from './report';
39
+ import { generateEntryPoint } from './transforms/v3/entry-point';
40
+ import { transformAgentFile } from './transforms/v3/agents';
41
+ import { generateServicesFile } from './transforms/v3/services';
42
+ import {
43
+ transformRouteServices,
44
+ computeServicesRelativePath,
45
+ removeRuntimeImports,
46
+ } from './transforms/v3/routes';
47
+ import { transformPackageJsonV3 } from './transforms/v3/package-json';
48
+ import { generateDevSetup } from './transforms/v3/dev-setup';
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Types
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export interface MigrateV3Options {
55
+ /** Project directory (defaults to cwd) */
56
+ projectDir?: string;
57
+ /** Skip interactive confirmation */
58
+ yes?: boolean;
59
+ /** Only run detection + print report, no transforms */
60
+ dryRun?: boolean;
61
+ }
62
+
63
+ export type MigrateV3Result = { ok: true; changedFiles: string[] } | { ok: false; reason: string };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Git helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ async function isGitWorktreeClean(projectDir: string): Promise<boolean> {
70
+ try {
71
+ const result = Bun.spawn(['git', 'status', '--porcelain'], {
72
+ cwd: projectDir,
73
+ stdout: 'pipe',
74
+ stderr: 'pipe',
75
+ });
76
+ const output = await new Response(result.stdout).text();
77
+ return output.trim() === '';
78
+ } catch {
79
+ return true;
80
+ }
81
+ }
82
+
83
+ function isGitRepo(projectDir: string): boolean {
84
+ try {
85
+ const result = Bun.spawnSync(['git', 'rev-parse', '--is-inside-work-tree'], {
86
+ cwd: projectDir,
87
+ stdout: 'pipe',
88
+ stderr: 'pipe',
89
+ });
90
+ return result.exitCode === 0;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Interactive confirm
98
+ // ---------------------------------------------------------------------------
99
+
100
+ async function confirm(message: string): Promise<boolean> {
101
+ process.stdout.write(` ${message} ${'\x1b[2m'}[y/N]${'\x1b[0m'} `);
102
+
103
+ const line = await new Promise<string>((resolve) => {
104
+ let buf = '';
105
+ process.stdin.setEncoding('utf8');
106
+ process.stdin.resume();
107
+ process.stdin.once('data', (chunk) => {
108
+ buf += chunk.toString();
109
+ process.stdin.pause();
110
+ resolve(buf.trim());
111
+ });
112
+ });
113
+
114
+ return line.toLowerCase() === 'y' || line.toLowerCase() === 'yes';
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Typecheck
119
+ // ---------------------------------------------------------------------------
120
+
121
+ async function runTypecheck(projectDir: string): Promise<{ ok: boolean; output: string }> {
122
+ try {
123
+ const proc = Bun.spawn(['bunx', 'tsc', '--noEmit', '--skipLibCheck'], {
124
+ cwd: projectDir,
125
+ stdout: 'pipe',
126
+ stderr: 'pipe',
127
+ });
128
+ await proc.exited;
129
+ const stdout = await new Response(proc.stdout).text();
130
+ const stderr = await new Response(proc.stderr).text();
131
+ const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
132
+ return { ok: proc.exitCode === 0, output: combined };
133
+ } catch (e) {
134
+ return { ok: false, output: String(e) };
135
+ }
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Bun install
140
+ // ---------------------------------------------------------------------------
141
+
142
+ async function runBunInstall(projectDir: string): Promise<{ ok: boolean; error?: string }> {
143
+ try {
144
+ const proc = Bun.spawn(['bun', 'install', '--silent'], {
145
+ cwd: projectDir,
146
+ stdout: 'pipe',
147
+ stderr: 'pipe',
148
+ });
149
+ await proc.exited;
150
+ return {
151
+ ok: proc.exitCode === 0,
152
+ error: proc.exitCode !== 0 ? 'bun install failed' : undefined,
153
+ };
154
+ } catch (e) {
155
+ return { ok: false, error: String(e) };
156
+ }
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Main
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3Result> {
164
+ const projectDir = resolve(opts.projectDir ?? process.cwd());
165
+
166
+ if (!existsSync(projectDir)) {
167
+ printError(`Project directory does not exist: ${projectDir}`);
168
+ return { ok: false, reason: 'Project directory not found' };
169
+ }
170
+
171
+ // ── 1. Git worktree check (skip for dry-run — no files are modified) ──
172
+ if (!opts.dryRun) {
173
+ if (isGitRepo(projectDir)) {
174
+ const isClean = await isGitWorktreeClean(projectDir);
175
+ if (!isClean) {
176
+ printError(
177
+ 'Git worktree is not clean.\n\n' +
178
+ ' The migration tool rewrites source files. Please commit or stash\n' +
179
+ ' your current changes before running the migration, so you can\n' +
180
+ ' easily review the diff or roll back if needed.\n\n' +
181
+ ' Run: git status\n' +
182
+ ' or: git stash'
183
+ );
184
+ return { ok: false, reason: 'Git worktree is not clean' };
185
+ }
186
+ } else {
187
+ printWarning(
188
+ 'Project is not in a git repository. It is strongly recommended to\n' +
189
+ ' version-control your project before running the migration.'
190
+ );
191
+ if (!opts.yes) {
192
+ const proceed = await confirm('Proceed without git? This cannot be undone.');
193
+ if (!proceed) {
194
+ console.log('\n Migration cancelled.\n');
195
+ return { ok: false, reason: 'User cancelled' };
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ // ── 2. Detection ──────────────────────────────────────────────────────
202
+ console.log('\n Scanning project for v2 patterns…');
203
+ const detection = await detectV3(projectDir);
204
+
205
+ // ── 3. Report ─────────────────────────────────────────────────────────
206
+ printV3Report(detection);
207
+
208
+ if (detection.findings.length === 0) {
209
+ return { ok: true, changedFiles: [] };
210
+ }
211
+
212
+ if (opts.dryRun) {
213
+ console.log(' (dry-run mode — no files modified)\n');
214
+ return { ok: true, changedFiles: [] };
215
+ }
216
+
217
+ const hasAuto = detection.findings.some((f) => f.severity === 'auto');
218
+ const hasGuided = detection.findings.some((f) => f.severity === 'guided');
219
+
220
+ if (!hasAuto && !hasGuided) {
221
+ console.log(' All findings require manual action. No automated transforms available.\n');
222
+ printManualSummaryV3(detection);
223
+ return { ok: true, changedFiles: [] };
224
+ }
225
+
226
+ // ── 4. Confirmation ───────────────────────────────────────────────────
227
+ if (!opts.yes) {
228
+ const proceed = await confirm('Apply the auto-fixable and guided transforms listed above?');
229
+ if (!proceed) {
230
+ console.log('\n Migration cancelled.\n');
231
+ return { ok: false, reason: 'User cancelled' };
232
+ }
233
+ }
234
+
235
+ console.log();
236
+
237
+ const changedFiles: string[] = [];
238
+ const allChangeSummary: { file: string; changes: string[] }[] = [];
239
+ let devScripts: Record<string, string> | undefined;
240
+
241
+ // ── 5a. Generate src/services.ts ──────────────────────────────────────
242
+ if (detection.allServicesUsed.length > 0) {
243
+ printStep('Generating src/services.ts');
244
+ const result = generateServicesFile(detection.allServicesUsed);
245
+
246
+ if (result.source) {
247
+ const servicesPath = join(projectDir, 'src', 'services.ts');
248
+ mkdirSync(dirname(servicesPath), { recursive: true });
249
+ writeFileSync(servicesPath, result.source, 'utf8');
250
+ changedFiles.push('src/services.ts');
251
+ allChangeSummary.push({ file: 'src/services.ts', changes: result.changes });
252
+ printStepDone(`${detection.allServicesUsed.length} service(s)`);
253
+ } else {
254
+ printStepSkipped('no services used');
255
+ }
256
+ }
257
+
258
+ // ── 5b. Transform agent files ─────────────────────────────────────────
259
+ if (detection.agentFiles.length > 0) {
260
+ console.log(`\n Transforming ${detection.agentFiles.length} agent file(s):`);
261
+
262
+ for (const agentFile of detection.agentFiles) {
263
+ printStep(agentFile.relativePath);
264
+
265
+ const src = await Bun.file(agentFile.path).text();
266
+ const servicesPath = computeServicesRelativePath(projectDir, agentFile.path);
267
+ const result = transformAgentFile(src, agentFile, servicesPath);
268
+
269
+ if (result.source !== null) {
270
+ writeFileSync(agentFile.path, result.source, 'utf8');
271
+ changedFiles.push(agentFile.relativePath);
272
+ allChangeSummary.push({ file: agentFile.relativePath, changes: result.changes });
273
+ if (result.manualRequired) {
274
+ printStepDone('migration comment added (manual review needed)');
275
+ } else {
276
+ printStepDone();
277
+ }
278
+ } else {
279
+ printStepSkipped(result.changes[0] ?? 'no changes');
280
+ }
281
+ }
282
+ }
283
+
284
+ // ── 5c. Rewrite service access in route files ─────────────────────────
285
+ const routeServiceUsages = detection.serviceUsages.filter(
286
+ (u) =>
287
+ u.accessPattern === 'c.var' &&
288
+ // Don't process app.ts (it's being replaced)
289
+ u.relativePath !== 'app.ts'
290
+ );
291
+
292
+ if (routeServiceUsages.length > 0) {
293
+ console.log(`\n Rewriting service access in ${routeServiceUsages.length} route file(s):`);
294
+
295
+ for (const usage of routeServiceUsages) {
296
+ printStep(usage.relativePath);
297
+
298
+ const src = await Bun.file(usage.path).text();
299
+ const servicesPath = computeServicesRelativePath(projectDir, usage.path);
300
+ const result = transformRouteServices(src, usage, servicesPath);
301
+
302
+ if (result.source !== null) {
303
+ writeFileSync(usage.path, result.source, 'utf8');
304
+ changedFiles.push(usage.relativePath);
305
+ allChangeSummary.push({ file: usage.relativePath, changes: result.changes });
306
+ printStepDone();
307
+ } else {
308
+ printStepSkipped('no changes needed');
309
+ }
310
+ }
311
+ }
312
+
313
+ // ── 5c′. Remove @agentuity/runtime imports from all source files ─────
314
+ {
315
+ const srcDir = join(projectDir, 'src');
316
+ if (existsSync(srcDir)) {
317
+ const runtimeImportFiles: string[] = [];
318
+
319
+ const walkForRuntimeImports = (dir: string) => {
320
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
321
+ const full = join(dir, entry.name);
322
+ if (entry.isDirectory()) {
323
+ if (['node_modules', 'dist', '.agentuity', '.git', 'web'].includes(entry.name))
324
+ continue;
325
+ walkForRuntimeImports(full);
326
+ } else if (
327
+ entry.isFile() &&
328
+ (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))
329
+ ) {
330
+ runtimeImportFiles.push(full);
331
+ }
332
+ }
333
+ };
334
+
335
+ walkForRuntimeImports(srcDir);
336
+
337
+ let removedCount = 0;
338
+ for (const file of runtimeImportFiles) {
339
+ const relPath = file.replace(projectDir + '/', '');
340
+ // Skip files we've already modified
341
+ if (changedFiles.includes(relPath)) continue;
342
+
343
+ const src = await Bun.file(file).text();
344
+ if (!src.includes('@agentuity/runtime')) continue;
345
+
346
+ const { source: cleaned, removed } = removeRuntimeImports(src);
347
+ if (removed) {
348
+ writeFileSync(file, cleaned, 'utf8');
349
+ changedFiles.push(relPath);
350
+ allChangeSummary.push({
351
+ file: relPath,
352
+ changes: ['Removed @agentuity/runtime imports'],
353
+ });
354
+ removedCount++;
355
+ }
356
+ }
357
+
358
+ if (removedCount > 0) {
359
+ printStep(`Removed @agentuity/runtime imports from ${removedCount} additional file(s)`);
360
+ printStepDone();
361
+ }
362
+ }
363
+ }
364
+
365
+ // ── 5d. Generate new entry point ──────────────────────────────────────
366
+ if (detection.hasCreateApp) {
367
+ printStep('Generating src/index.ts');
368
+ const result = generateEntryPoint(detection);
369
+
370
+ if (result.source) {
371
+ const entryPath = join(projectDir, 'src', 'index.ts');
372
+ mkdirSync(dirname(entryPath), { recursive: true });
373
+ writeFileSync(entryPath, result.source, 'utf8');
374
+ changedFiles.push('src/index.ts');
375
+ allChangeSummary.push({ file: 'src/index.ts', changes: result.changes });
376
+ printStepDone();
377
+ } else {
378
+ printStepSkipped('no createApp detected');
379
+ }
380
+ }
381
+
382
+ // ── 5d′. Set up dev workflow for Hono + SPA ───────────────────────
383
+ if (detection.hasFrontend && detection.hasViteConfig) {
384
+ printStep('Setting up dev workflow (Vite proxy + concurrent servers)');
385
+
386
+ const viteConfigPath = join(projectDir, 'vite.config.ts');
387
+ const viteSource = await Bun.file(viteConfigPath).text();
388
+ const devResult = generateDevSetup(viteSource);
389
+
390
+ // Patch vite.config.ts with proxy
391
+ if (devResult.viteConfig) {
392
+ writeFileSync(viteConfigPath, devResult.viteConfig, 'utf8');
393
+ changedFiles.push('vite.config.ts');
394
+ allChangeSummary.push({ file: 'vite.config.ts', changes: devResult.viteChanges });
395
+ }
396
+
397
+ // Store dev scripts — they'll be applied in the package.json transform
398
+ devScripts = {
399
+ dev: devResult.devScript,
400
+ 'server:api': devResult.serverScript,
401
+ };
402
+
403
+ printStepDone();
404
+ }
405
+
406
+ // ── 5e. Delete old app.ts ─────────────────────────────────────────────
407
+ if (detection.appTsPath && detection.hasCreateApp) {
408
+ printStep('Removing old app.ts');
409
+ try {
410
+ unlinkSync(detection.appTsPath);
411
+ changedFiles.push('app.ts (deleted)');
412
+ allChangeSummary.push({
413
+ file: 'app.ts',
414
+ changes: ['Deleted — replaced by src/index.ts'],
415
+ });
416
+ printStepDone('deleted');
417
+ } catch (e) {
418
+ printStepFailed(String(e));
419
+ }
420
+ }
421
+
422
+ // ── 5f. Delete agentuity.config.ts ────────────────────────────────────
423
+ if (detection.hasAgentuityConfig) {
424
+ printStep('Removing agentuity.config.ts');
425
+ try {
426
+ unlinkSync(join(projectDir, 'agentuity.config.ts'));
427
+ changedFiles.push('agentuity.config.ts (deleted)');
428
+ allChangeSummary.push({
429
+ file: 'agentuity.config.ts',
430
+ changes: ['Deleted — no longer used in v3'],
431
+ });
432
+ printStepDone('deleted');
433
+ } catch (e) {
434
+ printStepFailed(String(e));
435
+ }
436
+ }
437
+
438
+ // ── 5g. Delete src/agent/index.ts barrel ──────────────────────────────
439
+ if (detection.hasAgentBarrel) {
440
+ printStep('Removing src/agent/index.ts barrel');
441
+ try {
442
+ unlinkSync(join(projectDir, 'src', 'agent', 'index.ts'));
443
+ changedFiles.push('src/agent/index.ts (deleted)');
444
+ allChangeSummary.push({
445
+ file: 'src/agent/index.ts',
446
+ changes: ['Deleted — agents barrel not needed in v3'],
447
+ });
448
+ printStepDone('deleted');
449
+ } catch (e) {
450
+ printStepFailed(String(e));
451
+ }
452
+ }
453
+
454
+ // ── 5h. Update package.json ───────────────────────────────────────────
455
+ const packageJsonPath = join(projectDir, 'package.json');
456
+ if (existsSync(packageJsonPath)) {
457
+ printStep('Updating package.json');
458
+
459
+ try {
460
+ const currentContent = await Bun.file(packageJsonPath).text();
461
+ const result = transformPackageJsonV3(
462
+ currentContent,
463
+ detection.outdatedPackages,
464
+ detection.allServicesUsed,
465
+ {
466
+ removeRuntime: detection.hasRuntimeDep,
467
+ removeReact: detection.hasReactPackage,
468
+ devScripts,
469
+ }
470
+ );
471
+
472
+ if (result.content && result.changes.length > 0) {
473
+ writeFileSync(packageJsonPath, result.content, 'utf8');
474
+ changedFiles.push('package.json');
475
+ allChangeSummary.push({ file: 'package.json', changes: result.changes });
476
+ printStepDone(`${result.changes.length} change(s)`);
477
+ } else {
478
+ printStepSkipped('no changes needed');
479
+ }
480
+ } catch (e) {
481
+ printStepFailed(String(e));
482
+ }
483
+ }
484
+
485
+ console.log();
486
+
487
+ // ── Print applied changes ──────────────────────────────────────────────
488
+ printChangeSummary(allChangeSummary);
489
+
490
+ // ── 6. Install dependencies ───────────────────────────────────────────
491
+ if (existsSync(packageJsonPath) && changedFiles.length > 0) {
492
+ printStep('Running bun install');
493
+ const install = await runBunInstall(projectDir);
494
+ if (install.ok) {
495
+ printStepDone('done');
496
+ } else {
497
+ printStepFailed(String(install.error));
498
+ }
499
+ }
500
+
501
+ // ── 7. Typecheck ─────────────────────────────────────────────────────
502
+ const hasTsConfig = existsSync(join(projectDir, 'tsconfig.json'));
503
+ if (hasTsConfig && changedFiles.length > 0) {
504
+ printStep('Running TypeScript type check');
505
+ const tc = await runTypecheck(projectDir);
506
+ if (tc.ok) {
507
+ printStepDone('no errors');
508
+ } else {
509
+ printStepFailed('type errors found');
510
+ console.log('\n TypeScript errors after migration:');
511
+ console.log(
512
+ tc.output
513
+ .split('\n')
514
+ .map((l) => ` ${l}`)
515
+ .join('\n')
516
+ );
517
+ printWarning(
518
+ 'Type errors detected. Review the changes above and fix manually.\n' +
519
+ ' The git diff will show exactly what was changed.'
520
+ );
521
+ }
522
+ } else if (!hasTsConfig) {
523
+ printWarning('No tsconfig.json found — skipping type check.');
524
+ }
525
+
526
+ // ── 8. Manual summary ─────────────────────────────────────────────────
527
+ printManualSummaryV3(detection);
528
+
529
+ if (changedFiles.length > 0) {
530
+ printSuccess(
531
+ `Migration complete! ${changedFiles.length} file(s) modified.\n` +
532
+ ` Review the changes with: git diff`
533
+ );
534
+ } else {
535
+ printSuccess('Migration complete! No files needed to be changed.');
536
+ }
537
+
538
+ return { ok: true, changedFiles };
539
+ }
package/src/report.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { DetectionResult, Finding, Severity } from './detect';
10
+ import type { V3DetectionResult, V3Finding } from './detect-v3';
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // ANSI helpers (minimal, no deps)
@@ -193,3 +194,88 @@ export function printChangeSummary(allChanges: { file: string; changes: string[]
193
194
  }
194
195
  console.log();
195
196
  }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // V3 report functions
200
+ // ---------------------------------------------------------------------------
201
+
202
+ function renderV3Finding(f: V3Finding, index: number): string {
203
+ const label = SEVERITY_LABEL[f.severity];
204
+ const icon = SEVERITY_ICON[f.severity];
205
+ const num = dim(`${String(index + 1).padStart(2, ' ')}.`);
206
+ const file = f.file ? dim(` [${f.file}]`) : '';
207
+ const hint = f.hint ? `\n ${dim('↳ ' + f.hint.replace(/\n/g, '\n '))}` : '';
208
+
209
+ return ` ${num} [${label}] ${icon} ${f.message}${file}${hint}`;
210
+ }
211
+
212
+ export function printV3Report(detection: V3DetectionResult): void {
213
+ const { findings } = detection;
214
+
215
+ const autoFindings = findings.filter((f) => f.severity === 'auto');
216
+ const guidedFindings = findings.filter((f) => f.severity === 'guided');
217
+ const manualFindings = findings.filter((f) => f.severity === 'manual');
218
+
219
+ console.log(`\n${bold('━━━ Agentuity v2 → v3 Migration Report ━━━')}`);
220
+ console.log(dim(`Project: ${detection.projectDir}`));
221
+
222
+ if (findings.length === 0) {
223
+ console.log(
224
+ `\n${green('✓')} ${bold('No v2 patterns detected!')} ` +
225
+ `This project may already be on v3.\n`
226
+ );
227
+ return;
228
+ }
229
+
230
+ // Summary counts
231
+ console.log(
232
+ `\n${bold('Summary:')} ` +
233
+ `${green(String(autoFindings.length))} auto-fixable, ` +
234
+ `${yellow(String(guidedFindings.length))} guided, ` +
235
+ `${red(String(manualFindings.length))} manual`
236
+ );
237
+
238
+ // Info about v3
239
+ console.log(
240
+ `\n${dim('v3 is framework-agnostic: bring your own framework, use @agentuity/hono')}\n` +
241
+ `${dim('middleware for services, and dedicated packages for each service.')}`
242
+ );
243
+
244
+ if (autoFindings.length > 0) {
245
+ console.log(heading('Auto-fixable (will be applied automatically)'));
246
+ autoFindings.forEach((f, i) => console.log(renderV3Finding(f, i)));
247
+ }
248
+
249
+ if (guidedFindings.length > 0) {
250
+ console.log(heading('Guided (applied with your review)'));
251
+ guidedFindings.forEach((f, i) => console.log(renderV3Finding(f, i)));
252
+ }
253
+
254
+ if (manualFindings.length > 0) {
255
+ console.log(heading('Manual (requires human action — tool will not touch these)'));
256
+ manualFindings.forEach((f, i) => console.log(renderV3Finding(f, i)));
257
+ }
258
+
259
+ console.log(`\n${hr()}`);
260
+ console.log(
261
+ `${dim('Legend:')} ` +
262
+ `[${green(' auto ')}] fully automated ` +
263
+ `[${yellow(' guided ')}] applied + verify ` +
264
+ `[${red(' manual ')}] instructions only`
265
+ );
266
+ console.log();
267
+ }
268
+
269
+ export function printManualSummaryV3(detection: V3DetectionResult): void {
270
+ const manualFindings = detection.findings.filter((f) => f.severity === 'manual');
271
+ if (manualFindings.length === 0) return;
272
+
273
+ console.log(`\n${bold('━━━ Remaining Manual Steps ━━━')}\n`);
274
+ manualFindings.forEach((f, i) => {
275
+ console.log(` ${dim(`${i + 1}.`)} ${red('✗')} ${f.message}`);
276
+ if (f.file) console.log(` ${dim(`File: ${f.file}`)}`);
277
+ if (f.hint) {
278
+ console.log(`\n ${f.hint.split('\n').join('\n ')}\n`);
279
+ }
280
+ });
281
+ }