@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.
- package/README.md +203 -0
- package/bin/migrate.ts +60 -0
- package/dist/detect.d.ts +56 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +561 -0
- package/dist/detect.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +315 -0
- package/dist/migrate.js.map +1 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +159 -0
- package/dist/report.js.map +1 -0
- package/dist/transforms/app-ts.d.ts +29 -0
- package/dist/transforms/app-ts.d.ts.map +1 -0
- package/dist/transforms/app-ts.js +114 -0
- package/dist/transforms/app-ts.js.map +1 -0
- package/dist/transforms/barrels.d.ts +12 -0
- package/dist/transforms/barrels.d.ts.map +1 -0
- package/dist/transforms/barrels.js +103 -0
- package/dist/transforms/barrels.js.map +1 -0
- package/dist/transforms/generated.d.ts +7 -0
- package/dist/transforms/generated.d.ts.map +1 -0
- package/dist/transforms/generated.js +10 -0
- package/dist/transforms/generated.js.map +1 -0
- package/dist/transforms/routes.d.ts +49 -0
- package/dist/transforms/routes.d.ts.map +1 -0
- package/dist/transforms/routes.js +208 -0
- package/dist/transforms/routes.js.map +1 -0
- package/package.json +45 -0
- package/src/detect.ts +694 -0
- package/src/index.ts +9 -0
- package/src/migrate.ts +379 -0
- package/src/report.ts +195 -0
- package/src/transforms/app-ts.ts +144 -0
- package/src/transforms/barrels.ts +138 -0
- package/src/transforms/generated.ts +11 -0
- package/src/transforms/routes.ts +273 -0
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Check git worktree is clean (bail if not)
|
|
6
|
+
* 2. Run detection
|
|
7
|
+
* 3. Print report
|
|
8
|
+
* 4. Interactive confirmation (unless --yes)
|
|
9
|
+
* 5. Apply codemods
|
|
10
|
+
* 6. Run typecheck
|
|
11
|
+
* 7. Print final summary
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { detect } from './detect';
|
|
18
|
+
import {
|
|
19
|
+
printReport,
|
|
20
|
+
printStep,
|
|
21
|
+
printStepDone,
|
|
22
|
+
printStepFailed,
|
|
23
|
+
printStepSkipped,
|
|
24
|
+
printWarning,
|
|
25
|
+
printError,
|
|
26
|
+
printSuccess,
|
|
27
|
+
printManualSummary,
|
|
28
|
+
printChangeSummary,
|
|
29
|
+
} from './report';
|
|
30
|
+
import { deleteGeneratedDir } from './transforms/generated';
|
|
31
|
+
import { transformAppTs } from './transforms/app-ts';
|
|
32
|
+
import { transformRouteFile } from './transforms/routes';
|
|
33
|
+
import { generateAgentBarrel, generateApiBarrel } from './transforms/barrels';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export interface MigrateOptions {
|
|
40
|
+
/** Project directory (defaults to cwd) */
|
|
41
|
+
projectDir?: string;
|
|
42
|
+
/** Skip interactive confirmation */
|
|
43
|
+
yes?: boolean;
|
|
44
|
+
/** Only run detection + print report, no transforms */
|
|
45
|
+
dryRun?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type MigrateResult = { ok: true; changedFiles: string[] } | { ok: false; reason: string };
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Git worktree cleanliness check
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
async function isGitWorktreeClean(projectDir: string): Promise<boolean> {
|
|
55
|
+
try {
|
|
56
|
+
const result = await Bun.spawn(['git', 'status', '--porcelain'], {
|
|
57
|
+
cwd: projectDir,
|
|
58
|
+
stdout: 'pipe',
|
|
59
|
+
stderr: 'pipe',
|
|
60
|
+
});
|
|
61
|
+
const output = await new Response(result.stdout).text();
|
|
62
|
+
return output.trim() === '';
|
|
63
|
+
} catch {
|
|
64
|
+
// git not available or not a git repo — allow migration to proceed
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isGitRepo(projectDir: string): boolean {
|
|
70
|
+
try {
|
|
71
|
+
const result = Bun.spawnSync(['git', 'rev-parse', '--is-inside-work-tree'], {
|
|
72
|
+
cwd: projectDir,
|
|
73
|
+
stdout: 'pipe',
|
|
74
|
+
stderr: 'pipe',
|
|
75
|
+
});
|
|
76
|
+
return result.exitCode === 0;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Interactive confirm
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
async function confirm(message: string): Promise<boolean> {
|
|
87
|
+
process.stdout.write(` ${message} ${'\x1b[2m'}[y/N]${'\x1b[0m'} `);
|
|
88
|
+
|
|
89
|
+
// Read a single line from stdin
|
|
90
|
+
const line = await new Promise<string>((resolve) => {
|
|
91
|
+
let buf = '';
|
|
92
|
+
process.stdin.setEncoding('utf8');
|
|
93
|
+
process.stdin.resume();
|
|
94
|
+
process.stdin.once('data', (chunk) => {
|
|
95
|
+
buf += chunk.toString();
|
|
96
|
+
process.stdin.pause();
|
|
97
|
+
resolve(buf.trim());
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return line.toLowerCase() === 'y' || line.toLowerCase() === 'yes';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Typecheck
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
async function runTypecheck(projectDir: string): Promise<{ ok: boolean; output: string }> {
|
|
109
|
+
try {
|
|
110
|
+
const proc = Bun.spawn(['bunx', 'tsc', '--noEmit', '--skipLibCheck'], {
|
|
111
|
+
cwd: projectDir,
|
|
112
|
+
stdout: 'pipe',
|
|
113
|
+
stderr: 'pipe',
|
|
114
|
+
});
|
|
115
|
+
await proc.exited;
|
|
116
|
+
const stdout = await new Response(proc.stdout).text();
|
|
117
|
+
const stderr = await new Response(proc.stderr).text();
|
|
118
|
+
const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
119
|
+
return { ok: proc.exitCode === 0, output: combined };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return { ok: false, output: String(e) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Bun install
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
async function runBunInstall(projectDir: string): Promise<{ ok: boolean; error?: string }> {
|
|
130
|
+
try {
|
|
131
|
+
const proc = Bun.spawn(['bun', 'install', '--silent'], {
|
|
132
|
+
cwd: projectDir,
|
|
133
|
+
stdout: 'pipe',
|
|
134
|
+
stderr: 'pipe',
|
|
135
|
+
});
|
|
136
|
+
await proc.exited;
|
|
137
|
+
return {
|
|
138
|
+
ok: proc.exitCode === 0,
|
|
139
|
+
error: proc.exitCode !== 0 ? 'bun install failed' : undefined,
|
|
140
|
+
};
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return { ok: false, error: String(e) };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Main
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export async function migrate(opts: MigrateOptions = {}): Promise<MigrateResult> {
|
|
151
|
+
const projectDir = resolve(opts.projectDir ?? process.cwd());
|
|
152
|
+
|
|
153
|
+
if (!existsSync(projectDir)) {
|
|
154
|
+
printError(`Project directory does not exist: ${projectDir}`);
|
|
155
|
+
return { ok: false, reason: 'Project directory not found' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── 1. Git worktree check ──────────────────────────────────────────────
|
|
159
|
+
if (isGitRepo(projectDir)) {
|
|
160
|
+
const isClean = await isGitWorktreeClean(projectDir);
|
|
161
|
+
if (!isClean) {
|
|
162
|
+
printError(
|
|
163
|
+
'Git worktree is not clean.\n\n' +
|
|
164
|
+
' The migration tool rewrites source files. Please commit or stash\n' +
|
|
165
|
+
' your current changes before running the migration, so you can\n' +
|
|
166
|
+
' easily review the diff or roll back if needed.\n\n' +
|
|
167
|
+
' Run: git status\n' +
|
|
168
|
+
' or: git stash'
|
|
169
|
+
);
|
|
170
|
+
return { ok: false, reason: 'Git worktree is not clean' };
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
printWarning(
|
|
174
|
+
'Project is not in a git repository. It is strongly recommended to\n' +
|
|
175
|
+
' version-control your project before running the migration.'
|
|
176
|
+
);
|
|
177
|
+
if (!opts.yes) {
|
|
178
|
+
const proceed = await confirm('Proceed without git? This cannot be undone.');
|
|
179
|
+
if (!proceed) {
|
|
180
|
+
console.log('\n Migration cancelled.\n');
|
|
181
|
+
return { ok: false, reason: 'User cancelled' };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── 2. Detection ──────────────────────────────────────────────────────
|
|
187
|
+
console.log('\n Scanning project for v1 patterns…');
|
|
188
|
+
const detection = await detect(projectDir);
|
|
189
|
+
|
|
190
|
+
// ── 3. Report ─────────────────────────────────────────────────────────
|
|
191
|
+
printReport(detection);
|
|
192
|
+
|
|
193
|
+
if (detection.findings.length === 0) {
|
|
194
|
+
return { ok: true, changedFiles: [] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (opts.dryRun) {
|
|
198
|
+
console.log(' (dry-run mode — no files modified)\n');
|
|
199
|
+
return { ok: true, changedFiles: [] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Bail if there are manual-only findings with no automatable counterparts
|
|
203
|
+
const hasAuto = detection.findings.some((f) => f.severity === 'auto');
|
|
204
|
+
const hasGuided = detection.findings.some((f) => f.severity === 'guided');
|
|
205
|
+
|
|
206
|
+
if (!hasAuto && !hasGuided) {
|
|
207
|
+
console.log(' All findings require manual action. No automated transforms available.\n');
|
|
208
|
+
printManualSummary(detection);
|
|
209
|
+
return { ok: true, changedFiles: [] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── 4. Confirmation ───────────────────────────────────────────────────
|
|
213
|
+
if (!opts.yes) {
|
|
214
|
+
const proceed = await confirm('Apply the auto-fixable and guided transforms listed above?');
|
|
215
|
+
if (!proceed) {
|
|
216
|
+
console.log('\n Migration cancelled.\n');
|
|
217
|
+
return { ok: false, reason: 'User cancelled' };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log();
|
|
222
|
+
|
|
223
|
+
const changedFiles: string[] = [];
|
|
224
|
+
const allChangeSummary: { file: string; changes: string[] }[] = [];
|
|
225
|
+
|
|
226
|
+
// ── 5a. Delete src/generated/ ─────────────────────────────────────────
|
|
227
|
+
if (detection.generatedDir) {
|
|
228
|
+
printStep('Deleting src/generated/');
|
|
229
|
+
try {
|
|
230
|
+
deleteGeneratedDir(detection.generatedDir);
|
|
231
|
+
changedFiles.push('src/generated/');
|
|
232
|
+
printStepDone('directory removed');
|
|
233
|
+
} catch (e) {
|
|
234
|
+
printStepFailed(String(e));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Read original app.ts source before any transforms (needed by config transform)
|
|
239
|
+
const originalAppSrc = detection.appTsPath ? await Bun.file(detection.appTsPath).text() : null;
|
|
240
|
+
|
|
241
|
+
// ── 5b. Transform app.ts ──────────────────────────────────────────────
|
|
242
|
+
if (detection.appTsPath && originalAppSrc !== null) {
|
|
243
|
+
printStep('Transforming app.ts');
|
|
244
|
+
const result = transformAppTs(originalAppSrc, detection);
|
|
245
|
+
|
|
246
|
+
if (result.complexityError) {
|
|
247
|
+
printStepFailed('complexity guard triggered');
|
|
248
|
+
printWarning(result.complexityError);
|
|
249
|
+
} else if (result.source !== null && result.changes.length > 0) {
|
|
250
|
+
writeFileSync(detection.appTsPath, result.source, 'utf8');
|
|
251
|
+
changedFiles.push('app.ts');
|
|
252
|
+
allChangeSummary.push({ file: 'app.ts', changes: result.changes });
|
|
253
|
+
printStepDone(result.changes.length + ' change(s)');
|
|
254
|
+
} else {
|
|
255
|
+
printStepSkipped('no mechanical changes needed');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Note: analytics/workbench stay in createApp() in v2 - no config file migration needed
|
|
260
|
+
|
|
261
|
+
// ── 5c. Transform v1 route files ──────────────────────────────────────
|
|
262
|
+
if (detection.v1RouteFiles.length > 0) {
|
|
263
|
+
console.log(`\n Transforming ${detection.v1RouteFiles.length} route file(s):`);
|
|
264
|
+
for (const routeFile of detection.v1RouteFiles) {
|
|
265
|
+
const relPath = routeFile.replace(projectDir + '/', '');
|
|
266
|
+
printStep(relPath);
|
|
267
|
+
|
|
268
|
+
const src = await Bun.file(routeFile).text();
|
|
269
|
+
const result = transformRouteFile(src);
|
|
270
|
+
|
|
271
|
+
if (result.complexityError) {
|
|
272
|
+
printStepFailed('complexity guard triggered');
|
|
273
|
+
printWarning(`${relPath}: ${result.complexityError}`);
|
|
274
|
+
} else if (result.source !== null && result.changes.length > 0) {
|
|
275
|
+
writeFileSync(routeFile, result.source, 'utf8');
|
|
276
|
+
changedFiles.push(relPath);
|
|
277
|
+
allChangeSummary.push({ file: relPath, changes: result.changes });
|
|
278
|
+
printStepDone();
|
|
279
|
+
} else {
|
|
280
|
+
printStepSkipped('already v2 style');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── 5e. Generate src/api/index.ts barrel ─────────────────────────────
|
|
286
|
+
const apiBarrelFinding = detection.findings.find(
|
|
287
|
+
(f) => f.id === 'missing-api-barrel' || f.id === 'api-barrel-stub'
|
|
288
|
+
);
|
|
289
|
+
if (apiBarrelFinding) {
|
|
290
|
+
printStep('Generating src/api/index.ts barrel');
|
|
291
|
+
const barrel = generateApiBarrel(projectDir);
|
|
292
|
+
if (barrel) {
|
|
293
|
+
const apiIndexPath = join(projectDir, 'src', 'api', 'index.ts');
|
|
294
|
+
writeFileSync(apiIndexPath, barrel, 'utf8');
|
|
295
|
+
changedFiles.push('src/api/index.ts');
|
|
296
|
+
allChangeSummary.push({
|
|
297
|
+
file: 'src/api/index.ts',
|
|
298
|
+
changes: ['Generated API router barrel with AppRouter type export'],
|
|
299
|
+
});
|
|
300
|
+
printStepDone();
|
|
301
|
+
} else {
|
|
302
|
+
printStepSkipped('no route files found');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── 5f. Generate src/agent/index.ts barrel ────────────────────────────
|
|
307
|
+
if (!detection.hasAgentBarrel) {
|
|
308
|
+
printStep('Generating src/agent/index.ts barrel');
|
|
309
|
+
const barrel = generateAgentBarrel(projectDir);
|
|
310
|
+
if (barrel) {
|
|
311
|
+
const agentIndexPath = join(projectDir, 'src', 'agent', 'index.ts');
|
|
312
|
+
writeFileSync(agentIndexPath, barrel, 'utf8');
|
|
313
|
+
changedFiles.push('src/agent/index.ts');
|
|
314
|
+
allChangeSummary.push({
|
|
315
|
+
file: 'src/agent/index.ts',
|
|
316
|
+
changes: ['Generated agent barrel exporting default array of all agents'],
|
|
317
|
+
});
|
|
318
|
+
printStepDone();
|
|
319
|
+
} else {
|
|
320
|
+
printStepSkipped('no agent files found');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log();
|
|
325
|
+
|
|
326
|
+
// ── Print applied changes ──────────────────────────────────────────────
|
|
327
|
+
printChangeSummary(allChangeSummary);
|
|
328
|
+
|
|
329
|
+
// ── 6. Install dependencies ───────────────────────────────────────────
|
|
330
|
+
const hasPackageJson = existsSync(join(projectDir, 'package.json'));
|
|
331
|
+
if (hasPackageJson && changedFiles.length > 0) {
|
|
332
|
+
printStep('Running bun install');
|
|
333
|
+
const install = await runBunInstall(projectDir);
|
|
334
|
+
if (install.ok) {
|
|
335
|
+
printStepDone('done');
|
|
336
|
+
} else {
|
|
337
|
+
printStepFailed(String(install.error));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── 7. Typecheck ─────────────────────────────────────────────────────
|
|
342
|
+
const hasTsConfig = existsSync(join(projectDir, 'tsconfig.json'));
|
|
343
|
+
if (hasTsConfig && changedFiles.length > 0) {
|
|
344
|
+
printStep('Running TypeScript type check');
|
|
345
|
+
const tc = await runTypecheck(projectDir);
|
|
346
|
+
if (tc.ok) {
|
|
347
|
+
printStepDone('no errors');
|
|
348
|
+
} else {
|
|
349
|
+
printStepFailed('type errors found');
|
|
350
|
+
console.log('\n TypeScript errors after migration:');
|
|
351
|
+
console.log(
|
|
352
|
+
tc.output
|
|
353
|
+
.split('\n')
|
|
354
|
+
.map((l) => ` ${l}`)
|
|
355
|
+
.join('\n')
|
|
356
|
+
);
|
|
357
|
+
printWarning(
|
|
358
|
+
'Type errors detected. Review the changes above and fix manually.\n' +
|
|
359
|
+
' The git diff will show exactly what was changed.'
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
} else if (!hasTsConfig) {
|
|
363
|
+
printWarning('No tsconfig.json found — skipping type check.');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── 7. Manual summary ─────────────────────────────────────────────────
|
|
367
|
+
printManualSummary(detection);
|
|
368
|
+
|
|
369
|
+
if (changedFiles.length > 0) {
|
|
370
|
+
printSuccess(
|
|
371
|
+
`Migration complete! ${changedFiles.length} file(s) modified.\n` +
|
|
372
|
+
` Review the changes with: git diff`
|
|
373
|
+
);
|
|
374
|
+
} else {
|
|
375
|
+
printSuccess('Migration complete! No files needed to be changed.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { ok: true, changedFiles };
|
|
379
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal report renderer.
|
|
3
|
+
*
|
|
4
|
+
* Formats the DetectionResult into a clear, readable console report.
|
|
5
|
+
* No dependencies on @agentuity/cli's TUI — we use raw ANSI codes so that
|
|
6
|
+
* this package can be run standalone via `npx @agentuity/migrate`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DetectionResult, Finding, Severity } from './detect';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// ANSI helpers (minimal, no deps)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
16
|
+
|
|
17
|
+
const ansi = {
|
|
18
|
+
reset: isTTY ? '\x1b[0m' : '',
|
|
19
|
+
bold: isTTY ? '\x1b[1m' : '',
|
|
20
|
+
dim: isTTY ? '\x1b[2m' : '',
|
|
21
|
+
green: isTTY ? '\x1b[32m' : '',
|
|
22
|
+
yellow: isTTY ? '\x1b[33m' : '',
|
|
23
|
+
red: isTTY ? '\x1b[31m' : '',
|
|
24
|
+
cyan: isTTY ? '\x1b[36m' : '',
|
|
25
|
+
blue: isTTY ? '\x1b[34m' : '',
|
|
26
|
+
magenta: isTTY ? '\x1b[35m' : '',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function bold(s: string) {
|
|
30
|
+
return `${ansi.bold}${s}${ansi.reset}`;
|
|
31
|
+
}
|
|
32
|
+
function dim(s: string) {
|
|
33
|
+
return `${ansi.dim}${s}${ansi.reset}`;
|
|
34
|
+
}
|
|
35
|
+
function green(s: string) {
|
|
36
|
+
return `${ansi.green}${s}${ansi.reset}`;
|
|
37
|
+
}
|
|
38
|
+
function yellow(s: string) {
|
|
39
|
+
return `${ansi.yellow}${s}${ansi.reset}`;
|
|
40
|
+
}
|
|
41
|
+
function red(s: string) {
|
|
42
|
+
return `${ansi.red}${s}${ansi.reset}`;
|
|
43
|
+
}
|
|
44
|
+
function cyan(s: string) {
|
|
45
|
+
return `${ansi.cyan}${s}${ansi.reset}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SEVERITY_LABEL: Record<Severity, string> = {
|
|
49
|
+
auto: green(' auto '),
|
|
50
|
+
guided: yellow(' guided '),
|
|
51
|
+
manual: red(' manual '),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SEVERITY_ICON: Record<Severity, string> = {
|
|
55
|
+
auto: green('✓'),
|
|
56
|
+
guided: yellow('⚠'),
|
|
57
|
+
manual: red('✗'),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Render helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function hr(width = 70): string {
|
|
65
|
+
return dim('─'.repeat(width));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function heading(text: string): string {
|
|
69
|
+
return `\n${bold(text)}\n${hr()}\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderFinding(f: Finding, index: number): string {
|
|
73
|
+
const label = SEVERITY_LABEL[f.severity];
|
|
74
|
+
const icon = SEVERITY_ICON[f.severity];
|
|
75
|
+
const num = dim(`${String(index + 1).padStart(2, ' ')}.`);
|
|
76
|
+
const file = f.file ? dim(` [${f.file}]`) : '';
|
|
77
|
+
const hint = f.hint ? `\n ${dim('↳ ' + f.hint.replace(/\n/g, '\n '))}` : '';
|
|
78
|
+
|
|
79
|
+
return ` ${num} [${label}] ${icon} ${f.message}${file}${hint}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Public API
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export function printReport(detection: DetectionResult): void {
|
|
87
|
+
const { findings } = detection;
|
|
88
|
+
|
|
89
|
+
const autoFindings = findings.filter((f) => f.severity === 'auto');
|
|
90
|
+
const guidedFindings = findings.filter((f) => f.severity === 'guided');
|
|
91
|
+
const manualFindings = findings.filter((f) => f.severity === 'manual');
|
|
92
|
+
|
|
93
|
+
console.log(`\n${bold('━━━ Agentuity v1 → v2 Migration Report ━━━')}`);
|
|
94
|
+
console.log(dim(`Project: ${detection.projectDir}`));
|
|
95
|
+
|
|
96
|
+
if (findings.length === 0) {
|
|
97
|
+
console.log(
|
|
98
|
+
`\n${green('✓')} ${bold('No v1 patterns detected!')} ` +
|
|
99
|
+
`This project may already be on v2.\n`
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Summary counts
|
|
105
|
+
console.log(
|
|
106
|
+
`\n${bold('Summary:')} ` +
|
|
107
|
+
`${green(String(autoFindings.length))} auto-fixable, ` +
|
|
108
|
+
`${yellow(String(guidedFindings.length))} guided, ` +
|
|
109
|
+
`${red(String(manualFindings.length))} manual`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Auto-fixable
|
|
113
|
+
if (autoFindings.length > 0) {
|
|
114
|
+
console.log(heading('Auto-fixable (will be applied automatically)'));
|
|
115
|
+
autoFindings.forEach((f, i) => console.log(renderFinding(f, i)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Guided
|
|
119
|
+
if (guidedFindings.length > 0) {
|
|
120
|
+
console.log(heading('Guided (applied with your review)'));
|
|
121
|
+
guidedFindings.forEach((f, i) => console.log(renderFinding(f, i)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Manual
|
|
125
|
+
if (manualFindings.length > 0) {
|
|
126
|
+
console.log(heading('Manual (requires human action — tool will not touch these)'));
|
|
127
|
+
manualFindings.forEach((f, i) => console.log(renderFinding(f, i)));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Legend
|
|
131
|
+
console.log(`\n${hr()}`);
|
|
132
|
+
console.log(
|
|
133
|
+
`${dim('Legend:')} ` +
|
|
134
|
+
`[${green(' auto ')}] fully automated ` +
|
|
135
|
+
`[${yellow(' guided ')}] applied + verify ` +
|
|
136
|
+
`[${red(' manual ')}] instructions only`
|
|
137
|
+
);
|
|
138
|
+
console.log();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function printStep(message: string): void {
|
|
142
|
+
process.stdout.write(` ${cyan('›')} ${message}…`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function printStepDone(detail?: string): void {
|
|
146
|
+
const suffix = detail ? dim(` (${detail})`) : '';
|
|
147
|
+
console.log(` ${green('✓')}${suffix}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function printStepSkipped(reason: string): void {
|
|
151
|
+
console.log(` ${dim(`skipped — ${reason}`)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function printStepFailed(reason: string): void {
|
|
155
|
+
console.log(` ${red('✗')} ${reason}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function printWarning(message: string): void {
|
|
159
|
+
console.warn(`\n ${yellow('⚠')} ${yellow(bold('Warning:'))} ${message}\n`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function printError(message: string): void {
|
|
163
|
+
console.error(`\n ${red('✗')} ${red(bold('Error:'))} ${message}\n`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function printSuccess(message: string): void {
|
|
167
|
+
console.log(`\n${green('✓')} ${bold(message)}\n`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function printManualSummary(detection: DetectionResult): void {
|
|
171
|
+
const manualFindings = detection.findings.filter((f) => f.severity === 'manual');
|
|
172
|
+
if (manualFindings.length === 0) return;
|
|
173
|
+
|
|
174
|
+
console.log(`\n${bold('━━━ Remaining Manual Steps ━━━')}\n`);
|
|
175
|
+
manualFindings.forEach((f, i) => {
|
|
176
|
+
console.log(` ${dim(`${i + 1}.`)} ${red('✗')} ${f.message}`);
|
|
177
|
+
if (f.file) console.log(` ${dim(`File: ${f.file}`)}`);
|
|
178
|
+
if (f.hint) {
|
|
179
|
+
console.log(`\n ${f.hint.split('\n').join('\n ')}\n`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function printChangeSummary(allChanges: { file: string; changes: string[] }[]): void {
|
|
185
|
+
if (allChanges.length === 0) return;
|
|
186
|
+
|
|
187
|
+
console.log(`\n${bold('━━━ Applied Changes ━━━')}\n`);
|
|
188
|
+
for (const { file, changes } of allChanges) {
|
|
189
|
+
console.log(` ${cyan(file)}`);
|
|
190
|
+
for (const change of changes) {
|
|
191
|
+
console.log(` ${dim('→')} ${change}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
console.log();
|
|
195
|
+
}
|