@agentuity/migrate 3.0.0-alpha.6 → 3.0.0-beta.0
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/dist/detect-v3.d.ts.map +1 -1
- package/dist/detect-v3.js +4 -3
- package/dist/detect-v3.js.map +1 -1
- package/dist/migrate-v3.d.ts.map +1 -1
- package/dist/migrate-v3.js +188 -5
- package/dist/migrate-v3.js.map +1 -1
- package/dist/transforms/v3/agents.d.ts +10 -3
- package/dist/transforms/v3/agents.d.ts.map +1 -1
- package/dist/transforms/v3/agents.js +256 -7
- package/dist/transforms/v3/agents.js.map +1 -1
- package/dist/transforms/v3/package-json.d.ts +2 -0
- package/dist/transforms/v3/package-json.d.ts.map +1 -1
- package/dist/transforms/v3/package-json.js +48 -2
- package/dist/transforms/v3/package-json.js.map +1 -1
- package/dist/transforms/v3/routes.d.ts +63 -4
- package/dist/transforms/v3/routes.d.ts.map +1 -1
- package/dist/transforms/v3/routes.js +131 -15
- package/dist/transforms/v3/routes.js.map +1 -1
- package/dist/transforms/v3/schema-to-zod.d.ts +18 -0
- package/dist/transforms/v3/schema-to-zod.d.ts.map +1 -0
- package/dist/transforms/v3/schema-to-zod.js +140 -0
- package/dist/transforms/v3/schema-to-zod.js.map +1 -0
- package/package.json +3 -4
- package/src/detect-v3.ts +5 -3
- package/src/migrate-v3.ts +208 -4
- package/src/transforms/v3/agents.ts +289 -7
- package/src/transforms/v3/package-json.ts +52 -2
- package/src/transforms/v3/routes.ts +195 -7
- package/src/transforms/v3/schema-to-zod.ts +162 -0
package/src/migrate-v3.ts
CHANGED
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
* 8. Print final summary
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
existsSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
mkdirSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
} from 'node:fs';
|
|
24
31
|
import { join, resolve, dirname } from 'node:path';
|
|
25
32
|
|
|
26
33
|
import { detectV3 } from './detect-v3';
|
|
@@ -42,10 +49,15 @@ import { generateServicesFile } from './transforms/v3/services';
|
|
|
42
49
|
import {
|
|
43
50
|
transformRouteServices,
|
|
44
51
|
computeServicesRelativePath,
|
|
52
|
+
insertAfterImports,
|
|
45
53
|
removeRuntimeImports,
|
|
54
|
+
rewriteV2AgentMethods,
|
|
55
|
+
stripAgentuityValidators,
|
|
56
|
+
stubV2HonoContext,
|
|
46
57
|
} from './transforms/v3/routes';
|
|
47
58
|
import { transformPackageJsonV3 } from './transforms/v3/package-json';
|
|
48
59
|
import { generateDevSetup } from './transforms/v3/dev-setup';
|
|
60
|
+
import { schemaToZod } from './transforms/v3/schema-to-zod';
|
|
49
61
|
|
|
50
62
|
// ---------------------------------------------------------------------------
|
|
51
63
|
// Types
|
|
@@ -80,6 +92,14 @@ async function isGitWorktreeClean(projectDir: string): Promise<boolean> {
|
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
function readFileSyncSafe(path: string): string | null {
|
|
96
|
+
try {
|
|
97
|
+
return readFileSync(path, 'utf8');
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
83
103
|
function isGitRepo(projectDir: string): boolean {
|
|
84
104
|
try {
|
|
85
105
|
const result = Bun.spawnSync(['git', 'rev-parse', '--is-inside-work-tree'], {
|
|
@@ -255,6 +275,55 @@ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3R
|
|
|
255
275
|
}
|
|
256
276
|
}
|
|
257
277
|
|
|
278
|
+
// ── 5a′. Delete @agentuity/evals files ─────────────────────────────────
|
|
279
|
+
// The evals framework was removed entirely in v3. Any file that imports
|
|
280
|
+
// @agentuity/evals — conventionally '*eval.ts' or '*eval.tsx' alongside
|
|
281
|
+
// an agent — is dropped wholesale. Keeping it would produce unrecoverable
|
|
282
|
+
// typecheck errors because `agent.createEval()` no longer exists on the
|
|
283
|
+
// plain function that replaces the v2 agent.
|
|
284
|
+
{
|
|
285
|
+
const srcDir = join(projectDir, 'src');
|
|
286
|
+
const evalFiles: string[] = [];
|
|
287
|
+
const walkForEvals = (dir: string) => {
|
|
288
|
+
if (!existsSync(dir)) return;
|
|
289
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
290
|
+
const full = join(dir, entry.name);
|
|
291
|
+
if (entry.isDirectory()) {
|
|
292
|
+
if (['node_modules', 'dist', '.agentuity', '.git'].includes(entry.name)) continue;
|
|
293
|
+
walkForEvals(full);
|
|
294
|
+
} else if (
|
|
295
|
+
entry.isFile() &&
|
|
296
|
+
(entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))
|
|
297
|
+
) {
|
|
298
|
+
const content = readFileSyncSafe(full);
|
|
299
|
+
if (content && content.includes("from '@agentuity/evals'")) {
|
|
300
|
+
evalFiles.push(full);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
walkForEvals(srcDir);
|
|
306
|
+
|
|
307
|
+
for (const file of evalFiles) {
|
|
308
|
+
const rel = file.replace(projectDir + '/', '');
|
|
309
|
+
try {
|
|
310
|
+
unlinkSync(file);
|
|
311
|
+
changedFiles.push(rel);
|
|
312
|
+
allChangeSummary.push({
|
|
313
|
+
file: rel,
|
|
314
|
+
changes: ['Deleted — @agentuity/evals removed in v3'],
|
|
315
|
+
});
|
|
316
|
+
} catch {
|
|
317
|
+
// ignore
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (evalFiles.length > 0) {
|
|
322
|
+
printStep(`Deleted ${evalFiles.length} @agentuity/evals file(s)`);
|
|
323
|
+
printStepDone();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
258
327
|
// ── 5b. Transform agent files ─────────────────────────────────────────
|
|
259
328
|
if (detection.agentFiles.length > 0) {
|
|
260
329
|
console.log(`\n Transforming ${detection.agentFiles.length} agent file(s):`);
|
|
@@ -335,6 +404,7 @@ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3R
|
|
|
335
404
|
walkForRuntimeImports(srcDir);
|
|
336
405
|
|
|
337
406
|
let removedCount = 0;
|
|
407
|
+
let anyFileNeededEnvType = false;
|
|
338
408
|
for (const file of runtimeImportFiles) {
|
|
339
409
|
const relPath = file.replace(projectDir + '/', '');
|
|
340
410
|
// Skip files we've already modified
|
|
@@ -343,13 +413,52 @@ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3R
|
|
|
343
413
|
const src = await Bun.file(file).text();
|
|
344
414
|
if (!src.includes('@agentuity/runtime')) continue;
|
|
345
415
|
|
|
346
|
-
const
|
|
347
|
-
if (removed) {
|
|
416
|
+
const cleanup = removeRuntimeImports(src);
|
|
417
|
+
if (cleanup.removed) {
|
|
418
|
+
let cleaned = cleanup.source;
|
|
419
|
+
const extra: string[] = [];
|
|
420
|
+
|
|
421
|
+
// If this file imported Env, add a typed import from the
|
|
422
|
+
// local types helper. The helper file itself is emitted
|
|
423
|
+
// once later in this step.
|
|
424
|
+
if (cleanup.needsEnvType) {
|
|
425
|
+
anyFileNeededEnvType = true;
|
|
426
|
+
cleaned = insertAfterImports(
|
|
427
|
+
cleaned,
|
|
428
|
+
"import type { Env } from '../types/hono-env';"
|
|
429
|
+
);
|
|
430
|
+
extra.push("Added: import type { Env } from '../types/hono-env'");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// If the file had `validator` imported (or any <agent>.validator()
|
|
434
|
+
// middleware style), strip those call sites too — v3 has no
|
|
435
|
+
// equivalent.
|
|
436
|
+
const stripped = stripAgentuityValidators(cleaned);
|
|
437
|
+
if (stripped.changed) {
|
|
438
|
+
cleaned = stripped.source;
|
|
439
|
+
extra.push(...stripped.changes);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Rewrite v2 agent method invocations (<agent>.run → <agent>,
|
|
443
|
+
// c.req.valid('json') → await c.req.json()).
|
|
444
|
+
const agentRewrite = rewriteV2AgentMethods(cleaned);
|
|
445
|
+
if (agentRewrite.changed) {
|
|
446
|
+
cleaned = agentRewrite.source;
|
|
447
|
+
extra.push(...agentRewrite.changes);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Stub v2 Hono context (c.var.thread, c.var.sessionId).
|
|
451
|
+
const stub = stubV2HonoContext(cleaned);
|
|
452
|
+
if (stub.changed) {
|
|
453
|
+
cleaned = stub.source;
|
|
454
|
+
extra.push(...stub.changes);
|
|
455
|
+
}
|
|
456
|
+
|
|
348
457
|
writeFileSync(file, cleaned, 'utf8');
|
|
349
458
|
changedFiles.push(relPath);
|
|
350
459
|
allChangeSummary.push({
|
|
351
460
|
file: relPath,
|
|
352
|
-
changes: ['Removed @agentuity/runtime imports'],
|
|
461
|
+
changes: ['Removed @agentuity/runtime imports', ...extra],
|
|
353
462
|
});
|
|
354
463
|
removedCount++;
|
|
355
464
|
}
|
|
@@ -359,6 +468,48 @@ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3R
|
|
|
359
468
|
printStep(`Removed @agentuity/runtime imports from ${removedCount} additional file(s)`);
|
|
360
469
|
printStepDone();
|
|
361
470
|
}
|
|
471
|
+
void anyFileNeededEnvType; // subsumed by post-scan below
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── 5c″. Emit src/types/hono-env.ts if any changed file references it ───
|
|
476
|
+
// Both the route service rewrite (5c) and the runtime import cleanup (5c′)
|
|
477
|
+
// can emit `import type { Env } from '../types/hono-env'`. We create the
|
|
478
|
+
// helper file once here by scanning the final content of changed files.
|
|
479
|
+
{
|
|
480
|
+
let helperNeeded = false;
|
|
481
|
+
for (const rel of changedFiles) {
|
|
482
|
+
try {
|
|
483
|
+
const content = readFileSyncSafe(join(projectDir, rel));
|
|
484
|
+
if (content && /from ['"]\.\.\/types\/hono-env['"]/.test(content)) {
|
|
485
|
+
helperNeeded = true;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
// ignore
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (helperNeeded) {
|
|
494
|
+
const helperPath = join(projectDir, 'src', 'types', 'hono-env.ts');
|
|
495
|
+
if (!existsSync(helperPath)) {
|
|
496
|
+
mkdirSync(dirname(helperPath), { recursive: true });
|
|
497
|
+
const body =
|
|
498
|
+
'/**\n' +
|
|
499
|
+
' * Hono context variable type for Agentuity services.\n' +
|
|
500
|
+
' *\n' +
|
|
501
|
+
' * Generated by @agentuity/migrate during the v2 → v3 migration as the\n' +
|
|
502
|
+
" * replacement for `import type { Env } from '@agentuity/runtime'`.\n" +
|
|
503
|
+
' */\n' +
|
|
504
|
+
"import type { Services } from '@agentuity/hono';\n\n" +
|
|
505
|
+
'export type Env = { Variables: Services };\n';
|
|
506
|
+
writeFileSync(helperPath, body, 'utf8');
|
|
507
|
+
changedFiles.push('src/types/hono-env.ts');
|
|
508
|
+
allChangeSummary.push({
|
|
509
|
+
file: 'src/types/hono-env.ts',
|
|
510
|
+
changes: ['Created — replaces `Env` type from @agentuity/runtime'],
|
|
511
|
+
});
|
|
512
|
+
}
|
|
362
513
|
}
|
|
363
514
|
}
|
|
364
515
|
|
|
@@ -451,6 +602,58 @@ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3R
|
|
|
451
602
|
}
|
|
452
603
|
}
|
|
453
604
|
|
|
605
|
+
// ── 5g′. Port @agentuity/schema usage to zod ─────────────────────────
|
|
606
|
+
// Walk all .ts/.tsx files under src/ one more time and rewrite
|
|
607
|
+
// `import { s } from '@agentuity/schema'` + `s.*` calls to the zod
|
|
608
|
+
// equivalents. Track whether the rewrite actually fired anywhere — if
|
|
609
|
+
// it did, we'll add zod (and drop @agentuity/schema) in the package
|
|
610
|
+
// update step below.
|
|
611
|
+
let anyFilePortedToZod = false;
|
|
612
|
+
{
|
|
613
|
+
const srcDir = join(projectDir, 'src');
|
|
614
|
+
const walkAllTs = (dir: string): string[] => {
|
|
615
|
+
if (!existsSync(dir)) return [];
|
|
616
|
+
const out: string[] = [];
|
|
617
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
618
|
+
const full = join(dir, entry.name);
|
|
619
|
+
if (entry.isDirectory()) {
|
|
620
|
+
if (['node_modules', 'dist', '.agentuity', '.git'].includes(entry.name)) continue;
|
|
621
|
+
out.push(...walkAllTs(full));
|
|
622
|
+
} else if (
|
|
623
|
+
entry.isFile() &&
|
|
624
|
+
(entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))
|
|
625
|
+
) {
|
|
626
|
+
out.push(full);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
for (const file of walkAllTs(srcDir)) {
|
|
633
|
+
const rel = file.replace(projectDir + '/', '');
|
|
634
|
+
const src = readFileSyncSafe(file);
|
|
635
|
+
if (!src) continue;
|
|
636
|
+
const ported = schemaToZod(src);
|
|
637
|
+
if (ported.changed) {
|
|
638
|
+
writeFileSync(file, ported.source, 'utf8');
|
|
639
|
+
anyFilePortedToZod = true;
|
|
640
|
+
if (!changedFiles.includes(rel)) {
|
|
641
|
+
changedFiles.push(rel);
|
|
642
|
+
allChangeSummary.push({ file: rel, changes: ported.changes });
|
|
643
|
+
} else {
|
|
644
|
+
// Merge into the existing entry so we don't lose earlier changes.
|
|
645
|
+
const entry = allChangeSummary.find((c) => c.file === rel);
|
|
646
|
+
if (entry) entry.changes.push(...ported.changes);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (anyFilePortedToZod) {
|
|
652
|
+
printStep('Ported @agentuity/schema usage to zod');
|
|
653
|
+
printStepDone();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
454
657
|
// ── 5h. Update package.json ───────────────────────────────────────────
|
|
455
658
|
const packageJsonPath = join(projectDir, 'package.json');
|
|
456
659
|
if (existsSync(packageJsonPath)) {
|
|
@@ -465,6 +668,7 @@ export async function migrateV3(opts: MigrateV3Options = {}): Promise<MigrateV3R
|
|
|
465
668
|
{
|
|
466
669
|
removeRuntime: detection.hasRuntimeDep,
|
|
467
670
|
removeReact: detection.hasReactPackage,
|
|
671
|
+
addZod: anyFilePortedToZod,
|
|
468
672
|
devScripts,
|
|
469
673
|
}
|
|
470
674
|
);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transform: createAgent() → plain exported functions
|
|
3
3
|
*
|
|
4
|
+
* v3 is an "eject" — the createAgent() wrapper, ctx.* service magic, thread
|
|
5
|
+
* state, sessions and evals are all replaced by user-visible primitives.
|
|
6
|
+
*
|
|
4
7
|
* For "simple" agents (handler + optional schema only), we:
|
|
5
8
|
* 1. Remove the createAgent() wrapper
|
|
6
9
|
* 2. Extract the handler as a named exported async function
|
|
7
10
|
* 3. Preserve schema validation if present
|
|
8
|
-
* 4. Replace ctx.* service access with imports from services module
|
|
11
|
+
* 4. Replace ctx.* service access with imports from the services module
|
|
9
12
|
*
|
|
10
|
-
* For "complex" agents, we
|
|
11
|
-
*
|
|
13
|
+
* For "complex" agents (setup/shutdown/onEvent/ctx.config), we additionally:
|
|
14
|
+
* 5. Hoist setup() return values into module-level lazy-init singletons
|
|
15
|
+
* so the handler body still compiles after the ctx.config.* → direct
|
|
16
|
+
* reference rewrite.
|
|
17
|
+
* 6. Replace ctx.thread.*, ctx.sessionId, ctx.app.* with TODO comments —
|
|
18
|
+
* these concepts are gone in v3.
|
|
12
19
|
*
|
|
13
20
|
* We use a combination of regex and AST analysis — regex for the mechanical
|
|
14
21
|
* transforms (preserving formatting), AST for detection (already done in detect-v3).
|
|
@@ -45,7 +52,7 @@ export function transformAgentFile(
|
|
|
45
52
|
servicesRelativePath: string
|
|
46
53
|
): AgentTransformResult {
|
|
47
54
|
if (agentInfo.complexity === 'complex') {
|
|
48
|
-
return
|
|
55
|
+
return forceConvertComplexAgent(source, agentInfo, servicesRelativePath);
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
const changes: string[] = [];
|
|
@@ -117,7 +124,270 @@ export function transformAgentFile(
|
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
120
|
-
*
|
|
127
|
+
* Force-convert a "complex" agent (setup/shutdown/onEvent/ctx.config) to
|
|
128
|
+
* a plain exported async function plus module-level lazy-init singletons.
|
|
129
|
+
*
|
|
130
|
+
* v3 is an eject — we prefer working-but-TODO'd code over compilation errors
|
|
131
|
+
* that block the whole migration. Every hoisted singleton and every dropped
|
|
132
|
+
* feature is annotated so the user can review.
|
|
133
|
+
*/
|
|
134
|
+
function forceConvertComplexAgent(
|
|
135
|
+
source: string,
|
|
136
|
+
agentInfo: AgentFile,
|
|
137
|
+
servicesRelativePath: string
|
|
138
|
+
): AgentTransformResult {
|
|
139
|
+
const changes: string[] = [];
|
|
140
|
+
const sourceFile = ts.createSourceFile(agentInfo.path, source, ts.ScriptTarget.ESNext, true);
|
|
141
|
+
|
|
142
|
+
const extracted = extractHandlerFromCreateAgent(sourceFile, source);
|
|
143
|
+
if (!extracted) {
|
|
144
|
+
// Fall back to the comment-only path so the file at least still parses.
|
|
145
|
+
return addManualMigrationComment(source, agentInfo);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const setupInfo = extractSetupFromCreateAgent(sourceFile);
|
|
149
|
+
|
|
150
|
+
let output = source;
|
|
151
|
+
|
|
152
|
+
// Step 1: Remove createAgent import
|
|
153
|
+
output = removeCreateAgentImport(output);
|
|
154
|
+
changes.push('Removed createAgent import from @agentuity/runtime');
|
|
155
|
+
|
|
156
|
+
// Step 2: Add services import if needed
|
|
157
|
+
if (agentInfo.ctxServices.length > 0) {
|
|
158
|
+
const importLine = `import { ${agentInfo.ctxServices.join(', ')} } from '${servicesRelativePath}';`;
|
|
159
|
+
output = insertAfterImports(output, importLine);
|
|
160
|
+
changes.push(`Added services import: ${agentInfo.ctxServices.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Step 3: Rewrite handler body to remove ctx.* magic.
|
|
164
|
+
let handlerBody = extracted.handlerBody;
|
|
165
|
+
|
|
166
|
+
// 3a. ctx.config.<name> → (await get_<name>()) — wired to the hoisted
|
|
167
|
+
// module-level lazy init emitted in step 5 below. We wrap with parens so
|
|
168
|
+
// subsequent `.foo.bar` chains still parse correctly.
|
|
169
|
+
const configRefs = new Set<string>();
|
|
170
|
+
for (const ctxName of CTX_PARAM_NAMES) {
|
|
171
|
+
const pattern = new RegExp(`\\b${ctxName}\\.config\\.([A-Za-z_$][A-Za-z0-9_$]*)`, 'g');
|
|
172
|
+
handlerBody = handlerBody.replace(pattern, (_m, id: string) => {
|
|
173
|
+
configRefs.add(id);
|
|
174
|
+
return `(await get_${id}())`;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (configRefs.size > 0) {
|
|
178
|
+
changes.push(`Rewrote ctx.config.{${[...configRefs].join(', ')}} → (await get_‹key›())`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 3b. ctx.logger → logger (expected to come from services barrel).
|
|
182
|
+
for (const ctxName of CTX_PARAM_NAMES) {
|
|
183
|
+
handlerBody = handlerBody.replace(new RegExp(`\\b${ctxName}\\.logger\\b`, 'g'), 'logger');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3c. Replace ctx.<service> → <service> for all detected services.
|
|
187
|
+
for (const service of agentInfo.ctxServices) {
|
|
188
|
+
for (const ctxName of CTX_PARAM_NAMES) {
|
|
189
|
+
const pattern = new RegExp(`\\b${ctxName}\\.${service}\\b`, 'g');
|
|
190
|
+
handlerBody = handlerBody.replace(pattern, service);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3d. Replace ctx.thread / ctx.sessionId / ctx.app with obvious stubs.
|
|
195
|
+
// These concepts have no v3 equivalent. We rewrite whole ctx.<x>.*
|
|
196
|
+
// expression chains by scanning the handler AST so we don't mangle
|
|
197
|
+
// surrounding punctuation the way a pure regex would.
|
|
198
|
+
const droppedAccessors = new Set<string>();
|
|
199
|
+
const handlerSf = ts.createSourceFile('handler.ts', handlerBody, ts.ScriptTarget.ESNext, true);
|
|
200
|
+
const edits: Array<{ start: number; end: number; replacement: string }> = [];
|
|
201
|
+
|
|
202
|
+
// Walk expressions that START with a ctx-named identifier and whose full
|
|
203
|
+
// chain matches our pattern. We accumulate all unique full-chain ranges
|
|
204
|
+
// and, later, keep only the outermost chain at each location.
|
|
205
|
+
function visitExpr(node: ts.Node): void {
|
|
206
|
+
if (ts.isPropertyAccessExpression(node) || ts.isCallExpression(node)) {
|
|
207
|
+
// Find the leftmost identifier by walking .expression left.
|
|
208
|
+
let cur: ts.Node = node;
|
|
209
|
+
while (true) {
|
|
210
|
+
if (ts.isPropertyAccessExpression(cur)) {
|
|
211
|
+
cur = cur.expression;
|
|
212
|
+
} else if (ts.isCallExpression(cur)) {
|
|
213
|
+
cur = cur.expression;
|
|
214
|
+
} else if (ts.isElementAccessExpression(cur)) {
|
|
215
|
+
cur = cur.expression;
|
|
216
|
+
} else {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (ts.isIdentifier(cur) && CTX_PARAM_NAMES.includes(cur.text)) {
|
|
221
|
+
// Need to check the first property access is one of our removed keys
|
|
222
|
+
// by re-walking from the leaf.
|
|
223
|
+
const parent: ts.Node | undefined = cur.parent;
|
|
224
|
+
let firstProp: string | undefined;
|
|
225
|
+
if (parent && ts.isPropertyAccessExpression(parent) && parent.expression === cur) {
|
|
226
|
+
firstProp = parent.name.text;
|
|
227
|
+
}
|
|
228
|
+
if (firstProp && (firstProp === 'thread' || firstProp === 'app')) {
|
|
229
|
+
edits.push({
|
|
230
|
+
start: node.getStart(handlerSf),
|
|
231
|
+
end: node.getEnd(),
|
|
232
|
+
replacement: `(undefined /* v3: ${cur.text}.${firstProp} removed */ as any)`,
|
|
233
|
+
});
|
|
234
|
+
droppedAccessors.add(`${cur.text}.${firstProp}`);
|
|
235
|
+
} else if (firstProp === 'sessionId') {
|
|
236
|
+
edits.push({
|
|
237
|
+
start: node.getStart(handlerSf),
|
|
238
|
+
end: node.getEnd(),
|
|
239
|
+
replacement: "('v3-no-session-id' /* v3: ctx.sessionId removed */)",
|
|
240
|
+
});
|
|
241
|
+
droppedAccessors.add(`${cur.text}.sessionId`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
ts.forEachChild(node, visitExpr);
|
|
246
|
+
}
|
|
247
|
+
ts.forEachChild(handlerSf, visitExpr);
|
|
248
|
+
|
|
249
|
+
// Keep only the **outermost** ctx.x.y.z chain per location. The AST visitor
|
|
250
|
+
// emits overlapping edits for every nested property access, so sort by
|
|
251
|
+
// length descending and drop anything contained within a larger accepted
|
|
252
|
+
// edit.
|
|
253
|
+
if (edits.length > 0) {
|
|
254
|
+
edits.sort((a, b) => b.end - b.start - (a.end - a.start) || a.start - b.start);
|
|
255
|
+
const kept: Array<{ start: number; end: number; replacement: string }> = [];
|
|
256
|
+
for (const e of edits) {
|
|
257
|
+
const overlaps = kept.some((k) => !(e.end <= k.start || e.start >= k.end));
|
|
258
|
+
if (!overlaps) kept.push(e);
|
|
259
|
+
}
|
|
260
|
+
// Apply right-to-left so earlier offsets stay valid.
|
|
261
|
+
kept.sort((a, b) => b.start - a.start);
|
|
262
|
+
let body = handlerBody;
|
|
263
|
+
for (const e of kept) {
|
|
264
|
+
body = body.slice(0, e.start) + e.replacement + body.slice(e.end);
|
|
265
|
+
}
|
|
266
|
+
handlerBody = body;
|
|
267
|
+
}
|
|
268
|
+
if (droppedAccessors.size > 0) {
|
|
269
|
+
changes.push(`Stubbed out ctx accessors removed in v3: ${[...droppedAccessors].join(', ')}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Update extracted payload with rewritten body.
|
|
273
|
+
const rewrittenExtracted = { ...extracted, handlerBody };
|
|
274
|
+
|
|
275
|
+
// Step 4: Replace the createAgent() statement with the plain function.
|
|
276
|
+
output = replaceCreateAgentWithFunction(output, agentInfo, rewrittenExtracted);
|
|
277
|
+
changes.push(`Converted complex agent "${agentInfo.name}" to plain exported async function`);
|
|
278
|
+
|
|
279
|
+
// Step 5: Hoist setup() body to module-level singletons.
|
|
280
|
+
if (setupInfo && configRefs.size > 0) {
|
|
281
|
+
const singletonBlock = buildLazyInitBlock(configRefs, setupInfo);
|
|
282
|
+
output = insertAfterImports(output, singletonBlock);
|
|
283
|
+
changes.push(
|
|
284
|
+
`Hoisted setup() return values to module-level lazy init: ${[...configRefs].join(', ')}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Step 6: Prepend a review banner so the user knows this was auto-ejected.
|
|
289
|
+
const banner =
|
|
290
|
+
'// ℹ️ v3 migration — complex agent auto-ejected\n' +
|
|
291
|
+
`// Reason: ${agentInfo.complexityReason ?? 'unknown'}\n` +
|
|
292
|
+
'// Review the hoisted singletons, TODO comments, and the stubbed-out\n' +
|
|
293
|
+
'// thread/session accessors — these v2 features have no v3 equivalent.\n';
|
|
294
|
+
output = banner + output;
|
|
295
|
+
|
|
296
|
+
return { source: output, changes, manualRequired: true };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Build a module-level lazy init block from the setup() body.
|
|
301
|
+
*
|
|
302
|
+
* Output shape (singleton pattern):
|
|
303
|
+
*
|
|
304
|
+
* let _client: ReturnType<typeof __buildSetup>['client'] | undefined;
|
|
305
|
+
* function getClient() {
|
|
306
|
+
* return _client ?? (_client = (() => { ... setup body returns .client ... })());
|
|
307
|
+
* }
|
|
308
|
+
*
|
|
309
|
+
* For simplicity and to avoid having to type-reason about return shape, we
|
|
310
|
+
* emit a single IIFE that runs setup() on first access and caches per-key.
|
|
311
|
+
*/
|
|
312
|
+
function buildLazyInitBlock(keys: Set<string>, setup: { setupBodyText: string }): string {
|
|
313
|
+
const keyList = [...keys];
|
|
314
|
+
const lines: string[] = [];
|
|
315
|
+
lines.push(
|
|
316
|
+
'\n// v3: lazy-init singletons hoisted from the former setup() hook.\n' +
|
|
317
|
+
'// Each key is computed lazily on first access so we keep the original\n' +
|
|
318
|
+
'// semantics (constructors run at handler-time, not at module load).'
|
|
319
|
+
);
|
|
320
|
+
lines.push(`let __setupResult: Record<string, unknown> | undefined;`);
|
|
321
|
+
lines.push(`async function __runSetup(): Promise<Record<string, unknown>> {`);
|
|
322
|
+
lines.push(`\tif (__setupResult) return __setupResult;`);
|
|
323
|
+
lines.push(
|
|
324
|
+
`\t// Original setup() body — adjust by hand if it referenced ctx:\n\tconst __result = await (async () => ${setup.setupBodyText})();`
|
|
325
|
+
);
|
|
326
|
+
lines.push(`\t__setupResult = __result as Record<string, unknown>;`);
|
|
327
|
+
lines.push(`\treturn __setupResult;`);
|
|
328
|
+
lines.push(`}`);
|
|
329
|
+
for (const key of keyList) {
|
|
330
|
+
lines.push(
|
|
331
|
+
`async function get_${key}() { return (await __runSetup())[${JSON.stringify(key)}] as any; }`
|
|
332
|
+
);
|
|
333
|
+
// We also expose a sync alias for cases where the original code read ctx.config.X
|
|
334
|
+
// synchronously — the user will need to await. We emit a `const X = await get_X()` stub
|
|
335
|
+
// near the start of the handler via the handler-body rewriter later on.
|
|
336
|
+
}
|
|
337
|
+
return lines.join('\n') + '\n';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Extract the body of the setup() property/method on the createAgent() config.
|
|
342
|
+
*/
|
|
343
|
+
function extractSetupFromCreateAgent(sourceFile: ts.SourceFile): { setupBodyText: string } | null {
|
|
344
|
+
let result: { setupBodyText: string } | null = null;
|
|
345
|
+
|
|
346
|
+
function visit(node: ts.Node) {
|
|
347
|
+
if (result) return;
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
ts.isCallExpression(node) &&
|
|
351
|
+
ts.isIdentifier(node.expression) &&
|
|
352
|
+
node.expression.text === 'createAgent'
|
|
353
|
+
) {
|
|
354
|
+
const configArg = node.arguments[1];
|
|
355
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg)) return;
|
|
356
|
+
|
|
357
|
+
for (const prop of configArg.properties) {
|
|
358
|
+
const name =
|
|
359
|
+
(ts.isPropertyAssignment(prop) || ts.isMethodDeclaration(prop)) &&
|
|
360
|
+
ts.isIdentifier(prop.name)
|
|
361
|
+
? prop.name.text
|
|
362
|
+
: undefined;
|
|
363
|
+
if (name !== 'setup') continue;
|
|
364
|
+
|
|
365
|
+
let body: ts.Block | ts.Expression | undefined;
|
|
366
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
367
|
+
const init = prop.initializer;
|
|
368
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
369
|
+
body = init.body;
|
|
370
|
+
}
|
|
371
|
+
} else if (ts.isMethodDeclaration(prop)) {
|
|
372
|
+
body = prop.body;
|
|
373
|
+
}
|
|
374
|
+
if (!body) continue;
|
|
375
|
+
|
|
376
|
+
const bodyText = body.getText(sourceFile);
|
|
377
|
+
result = { setupBodyText: bodyText };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
ts.forEachChild(node, visit);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
visit(sourceFile);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* For complex agents we cannot auto-migrate, add a migration comment block
|
|
390
|
+
* only (fallback if the handler couldn't be extracted).
|
|
121
391
|
*/
|
|
122
392
|
function addManualMigrationComment(source: string, agentInfo: AgentFile): AgentTransformResult {
|
|
123
393
|
const comment =
|
|
@@ -214,7 +484,12 @@ function extractHandlerFromCreateAgent(
|
|
|
214
484
|
|
|
215
485
|
if (!body) return;
|
|
216
486
|
|
|
217
|
-
// Get parameter text (skip ctx/context parameter, keep input)
|
|
487
|
+
// Get parameter text (skip ctx/context parameter, keep input).
|
|
488
|
+
// We also ensure every surviving param has a type annotation — the v2
|
|
489
|
+
// scaffold relied on createAgent<Schema>() to type the handler's input,
|
|
490
|
+
// which is gone in v3. Rather than lose type information, we fall back
|
|
491
|
+
// to `: unknown` + a TODO so the resulting plain function compiles
|
|
492
|
+
// under strict mode. Users can tighten the type by parsing with zod.
|
|
218
493
|
const paramTexts: string[] = [];
|
|
219
494
|
if (params) {
|
|
220
495
|
for (let i = 0; i < params.length; i++) {
|
|
@@ -223,7 +498,14 @@ function extractHandlerFromCreateAgent(
|
|
|
223
498
|
const paramName = param.name.getText(sourceFile);
|
|
224
499
|
// Skip the first param if it's ctx/context (agent context)
|
|
225
500
|
if (i === 0 && CTX_PARAM_NAMES.includes(paramName)) continue;
|
|
226
|
-
|
|
501
|
+
let paramText = param.getText(sourceFile);
|
|
502
|
+
// If the param has no type annotation (v2 scaffold relied on
|
|
503
|
+
// createAgent generics), inject `: any` so the plain function
|
|
504
|
+
// still compiles.
|
|
505
|
+
if (!param.type) {
|
|
506
|
+
paramText += ': any';
|
|
507
|
+
}
|
|
508
|
+
paramTexts.push(paramText);
|
|
227
509
|
}
|
|
228
510
|
}
|
|
229
511
|
|
|
@@ -10,6 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
import { SERVICE_PACKAGE_MAP, type V3OutdatedPackage } from '../../detect-v3';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Packages that existed in v2 but are removed in v3.
|
|
15
|
+
*
|
|
16
|
+
* v3 is a deliberate "eject" — agent framework magic (evals, workbench) and
|
|
17
|
+
* helper facades (frontend, react) are replaced by user-visible primitives.
|
|
18
|
+
* These packages have no v3 counterpart and must be deleted from package.json,
|
|
19
|
+
* not bumped to a non-existent ^3.0.0.
|
|
20
|
+
*
|
|
21
|
+
* Note: @agentuity/react is handled separately via options.removeReact.
|
|
22
|
+
*/
|
|
23
|
+
const PACKAGES_REMOVED_IN_V3 = [
|
|
24
|
+
'@agentuity/evals',
|
|
25
|
+
'@agentuity/frontend',
|
|
26
|
+
'@agentuity/workbench',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
13
29
|
export interface V3PackageJsonResult {
|
|
14
30
|
/** Transformed package.json content, or null if no changes */
|
|
15
31
|
content: string | null;
|
|
@@ -29,6 +45,8 @@ export function transformPackageJsonV3(
|
|
|
29
45
|
removeRuntime?: boolean;
|
|
30
46
|
/** Whether to remove @agentuity/react */
|
|
31
47
|
removeReact?: boolean;
|
|
48
|
+
/** Whether any source file was ported from @agentuity/schema to zod */
|
|
49
|
+
addZod?: boolean;
|
|
32
50
|
/** Dev scripts to add (from dev-setup transform) */
|
|
33
51
|
devScripts?: Record<string, string>;
|
|
34
52
|
}
|
|
@@ -87,8 +105,24 @@ export function transformPackageJsonV3(
|
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
// ──
|
|
108
|
+
// ── 5a. Remove v2-only packages that have no v3 counterpart ────────────
|
|
109
|
+
// (These would otherwise be bumped to ^3.0.0 in step 5b, which fails to
|
|
110
|
+
// resolve because the packages were deleted entirely in v3.)
|
|
111
|
+
for (const removed of PACKAGES_REMOVED_IN_V3) {
|
|
112
|
+
if (deps[removed]) {
|
|
113
|
+
delete deps[removed];
|
|
114
|
+
changes.push(`Removed ${removed} from dependencies (no longer exists in v3)`);
|
|
115
|
+
}
|
|
116
|
+
if (devDeps[removed]) {
|
|
117
|
+
delete devDeps[removed];
|
|
118
|
+
changes.push(`Removed ${removed} from devDependencies (no longer exists in v3)`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── 5b. Bump existing @agentuity/* packages to ^3.0.0 ─────────────────
|
|
123
|
+
// Skip packages we just removed — they'd otherwise come back.
|
|
91
124
|
for (const outdated of outdatedPackages) {
|
|
125
|
+
if ((PACKAGES_REMOVED_IN_V3 as readonly string[]).includes(outdated.name)) continue;
|
|
92
126
|
const section = outdated.section === 'dependencies' ? deps : devDeps;
|
|
93
127
|
if (section[outdated.name]) {
|
|
94
128
|
section[outdated.name] = '^3.0.0';
|
|
@@ -96,7 +130,23 @@ export function transformPackageJsonV3(
|
|
|
96
130
|
}
|
|
97
131
|
}
|
|
98
132
|
|
|
99
|
-
// ── 6.
|
|
133
|
+
// ── 6. Add zod + remove @agentuity/schema when the schema→zod port fired ──
|
|
134
|
+
if (options?.addZod) {
|
|
135
|
+
if (!deps['zod']) {
|
|
136
|
+
deps['zod'] = '^4.0.0';
|
|
137
|
+
changes.push('Added zod@^4.0.0 to dependencies');
|
|
138
|
+
}
|
|
139
|
+
if (deps['@agentuity/schema']) {
|
|
140
|
+
delete deps['@agentuity/schema'];
|
|
141
|
+
changes.push('Removed @agentuity/schema (usage ported to zod)');
|
|
142
|
+
}
|
|
143
|
+
if (devDeps['@agentuity/schema']) {
|
|
144
|
+
delete devDeps['@agentuity/schema'];
|
|
145
|
+
changes.push('Removed @agentuity/schema from devDependencies (usage ported to zod)');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── 7. Remove @agentuity/react if requested ───────────────────────────
|
|
100
150
|
if (options?.removeReact) {
|
|
101
151
|
if (deps['@agentuity/react']) {
|
|
102
152
|
delete deps['@agentuity/react'];
|