@aion0/forge 0.5.48 → 0.5.49
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/CLAUDE.md +0 -1
- package/RELEASE_NOTES.md +8 -3
- package/app/api/tasks/[id]/log/entry/route.ts +13 -0
- package/app/api/tasks/[id]/log/route.ts +23 -0
- package/app/api/tasks/route.ts +2 -2
- package/components/ProjectDetail.tsx +1 -16
- package/components/TaskDetail.tsx +201 -51
- package/lib/help-docs/CLAUDE.md +0 -2
- package/lib/task-manager.ts +110 -0
- package/package.json +1 -1
- package/src/types/index.ts +7 -0
- package/app/api/migration/config/route.ts +0 -19
- package/app/api/migration/discover/route.ts +0 -26
- package/app/api/migration/failures/route.ts +0 -35
- package/app/api/migration/fix/route.ts +0 -82
- package/app/api/migration/run/route.ts +0 -22
- package/app/api/migration/run-batch/route.ts +0 -86
- package/components/MigrationCockpit.tsx +0 -541
- package/lib/help-docs/14-migration.md +0 -154
- package/lib/migration/differ.ts +0 -193
- package/lib/migration/discoverer.ts +0 -363
- package/lib/migration/openapi.ts +0 -137
- package/lib/migration/runner.ts +0 -219
- package/lib/migration/store.ts +0 -89
- package/lib/migration/types.ts +0 -115
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { loadConfig, saveConfig } from '@/lib/migration/store';
|
|
3
|
-
import type { MigrationConfig } from '@/lib/migration/types';
|
|
4
|
-
|
|
5
|
-
// GET /api/migration/config?projectPath=...
|
|
6
|
-
export async function GET(req: Request) {
|
|
7
|
-
const url = new URL(req.url);
|
|
8
|
-
const projectPath = url.searchParams.get('projectPath');
|
|
9
|
-
if (!projectPath) return NextResponse.json({ error: 'projectPath required' }, { status: 400 });
|
|
10
|
-
return NextResponse.json(loadConfig(projectPath));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// POST /api/migration/config — body: { projectPath, config }
|
|
14
|
-
export async function POST(req: Request) {
|
|
15
|
-
const { projectPath, config } = await req.json() as { projectPath: string; config: MigrationConfig };
|
|
16
|
-
if (!projectPath || !config) return NextResponse.json({ error: 'projectPath + config required' }, { status: 400 });
|
|
17
|
-
saveConfig(projectPath, config);
|
|
18
|
-
return NextResponse.json({ ok: true });
|
|
19
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { discoverEndpoints } from '@/lib/migration/discoverer';
|
|
3
|
-
import { loadConfig, saveEndpoints, loadEndpoints } from '@/lib/migration/store';
|
|
4
|
-
|
|
5
|
-
// GET /api/migration/discover?projectPath=... → return cached endpoints
|
|
6
|
-
export async function GET(req: Request) {
|
|
7
|
-
const url = new URL(req.url);
|
|
8
|
-
const projectPath = url.searchParams.get('projectPath');
|
|
9
|
-
if (!projectPath) return NextResponse.json({ error: 'projectPath required' }, { status: 400 });
|
|
10
|
-
return NextResponse.json({ endpoints: loadEndpoints(projectPath) });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// POST /api/migration/discover — body: { projectPath } → re-scan docs
|
|
14
|
-
export async function POST(req: Request) {
|
|
15
|
-
const { projectPath } = await req.json() as { projectPath: string };
|
|
16
|
-
if (!projectPath) return NextResponse.json({ error: 'projectPath required' }, { status: 400 });
|
|
17
|
-
const config = loadConfig(projectPath);
|
|
18
|
-
const result = discoverEndpoints(projectPath, config);
|
|
19
|
-
saveEndpoints(projectPath, result.endpoints);
|
|
20
|
-
return NextResponse.json({
|
|
21
|
-
endpoints: result.endpoints,
|
|
22
|
-
warnings: result.warnings,
|
|
23
|
-
sources: result.sources,
|
|
24
|
-
total: result.endpoints.length,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { loadFailures } from '@/lib/migration/store';
|
|
3
|
-
import type { Failure, FailureCluster } from '@/lib/migration/types';
|
|
4
|
-
|
|
5
|
-
function cluster(failures: Failure[]): FailureCluster[] {
|
|
6
|
-
const byType = new Map<string, Map<string, Failure[]>>();
|
|
7
|
-
for (const f of failures) {
|
|
8
|
-
let m = byType.get(f.errorType);
|
|
9
|
-
if (!m) { m = new Map(); byType.set(f.errorType, m); }
|
|
10
|
-
let arr = m.get(f.controller);
|
|
11
|
-
if (!arr) { arr = []; m.set(f.controller, arr); }
|
|
12
|
-
arr.push(f);
|
|
13
|
-
}
|
|
14
|
-
const out: FailureCluster[] = [];
|
|
15
|
-
for (const [errorType, ctrlMap] of byType) {
|
|
16
|
-
const controllers = [...ctrlMap.entries()].map(([controller, failures]) => ({ controller, failures }));
|
|
17
|
-
controllers.sort((a, b) => b.failures.length - a.failures.length);
|
|
18
|
-
out.push({
|
|
19
|
-
errorType,
|
|
20
|
-
count: controllers.reduce((sum, c) => sum + c.failures.length, 0),
|
|
21
|
-
controllers,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
out.sort((a, b) => b.count - a.count);
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// GET /api/migration/failures?projectPath=...
|
|
29
|
-
export async function GET(req: Request) {
|
|
30
|
-
const url = new URL(req.url);
|
|
31
|
-
const projectPath = url.searchParams.get('projectPath');
|
|
32
|
-
if (!projectPath) return NextResponse.json({ error: 'projectPath required' }, { status: 400 });
|
|
33
|
-
const failures = loadFailures(projectPath);
|
|
34
|
-
return NextResponse.json({ failures, clusters: cluster(failures) });
|
|
35
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { loadEndpoints } from '@/lib/migration/store';
|
|
7
|
-
import { createTask } from '@/lib/task-manager';
|
|
8
|
-
import type { Failure } from '@/lib/migration/types';
|
|
9
|
-
|
|
10
|
-
interface FixRequest {
|
|
11
|
-
projectPath: string;
|
|
12
|
-
projectName?: string;
|
|
13
|
-
mode: 'inject' | 'task';
|
|
14
|
-
endpointIds?: string[]; // single or batch
|
|
15
|
-
failures?: Failure[]; // optional pre-clustered failures
|
|
16
|
-
sessionName?: string; // tmux session for inject mode
|
|
17
|
-
customPrompt?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function buildPrompt(eps: any[], failures?: Failure[], custom?: string): string {
|
|
21
|
-
const lines: string[] = [];
|
|
22
|
-
lines.push('# API parity fix request');
|
|
23
|
-
lines.push('');
|
|
24
|
-
lines.push('The following endpoints in the new web-server module produce results that differ from the legacy module. The legacy code MUST NOT be changed — fix the new module so its output matches.');
|
|
25
|
-
lines.push('');
|
|
26
|
-
if (failures && failures.length > 0) {
|
|
27
|
-
lines.push('## Failures');
|
|
28
|
-
for (const f of failures) {
|
|
29
|
-
lines.push(`- \`${f.method} ${f.path}\` (${f.controller}) — ${f.errorType}: ${f.errorMessage}`);
|
|
30
|
-
}
|
|
31
|
-
} else if (eps.length > 0) {
|
|
32
|
-
lines.push('## Endpoints');
|
|
33
|
-
for (const e of eps) {
|
|
34
|
-
lines.push(`- \`${e.method} ${e.path}\` — controller: ${e.controller}, status: ${e.status}${e.notes ? `, notes: ${e.notes}` : ''}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
lines.push('');
|
|
38
|
-
lines.push('## Approach');
|
|
39
|
-
lines.push('1. Read .forge/migration/runs/*.json for the latest run output and diffs.');
|
|
40
|
-
lines.push('2. Identify the controller class in the new web-server module.');
|
|
41
|
-
lines.push('3. Compare to the legacy implementation — focus on response shape, status codes, field names.');
|
|
42
|
-
lines.push('4. Apply minimal fix; do not change unrelated code.');
|
|
43
|
-
lines.push('5. Re-run the migration cockpit batch test for these endpoints.');
|
|
44
|
-
if (custom) {
|
|
45
|
-
lines.push('');
|
|
46
|
-
lines.push('## Additional context');
|
|
47
|
-
lines.push(custom);
|
|
48
|
-
}
|
|
49
|
-
return lines.join('\n');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function POST(req: Request) {
|
|
53
|
-
const body = await req.json() as FixRequest;
|
|
54
|
-
const { projectPath, projectName, mode, endpointIds, failures, sessionName, customPrompt } = body;
|
|
55
|
-
if (!projectPath || !mode) return NextResponse.json({ error: 'projectPath + mode required' }, { status: 400 });
|
|
56
|
-
|
|
57
|
-
const all = loadEndpoints(projectPath);
|
|
58
|
-
const ids = new Set(endpointIds || []);
|
|
59
|
-
const eps = ids.size > 0 ? all.filter(e => ids.has(e.id)) : [];
|
|
60
|
-
const prompt = buildPrompt(eps, failures, customPrompt);
|
|
61
|
-
|
|
62
|
-
if (mode === 'task') {
|
|
63
|
-
const name = projectName || projectPath.split('/').filter(Boolean).pop() || 'project';
|
|
64
|
-
const task = createTask({ projectName: name, projectPath, prompt });
|
|
65
|
-
return NextResponse.json({ ok: true, mode: 'task', taskId: task.id });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (mode === 'inject') {
|
|
69
|
-
if (!sessionName) return NextResponse.json({ error: 'sessionName required for inject mode' }, { status: 400 });
|
|
70
|
-
try {
|
|
71
|
-
const buf = join(tmpdir(), `forge-migration-fix-${Date.now()}.txt`);
|
|
72
|
-
writeFileSync(buf, prompt);
|
|
73
|
-
execSync(`tmux load-buffer -t "${sessionName}" "${buf}" && tmux paste-buffer -t "${sessionName}" && sleep 0.2 && tmux send-keys -t "${sessionName}" Enter`, { timeout: 5000 });
|
|
74
|
-
try { unlinkSync(buf); } catch {}
|
|
75
|
-
return NextResponse.json({ ok: true, mode: 'inject', sessionName });
|
|
76
|
-
} catch (e: any) {
|
|
77
|
-
return NextResponse.json({ error: e?.message || String(e) }, { status: 500 });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return NextResponse.json({ error: 'invalid mode' }, { status: 400 });
|
|
82
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { loadConfig, loadEndpoints, saveRun } from '@/lib/migration/store';
|
|
3
|
-
import { runEndpoint } from '@/lib/migration/runner';
|
|
4
|
-
import { loadOpenApi } from '@/lib/migration/openapi';
|
|
5
|
-
|
|
6
|
-
// POST /api/migration/run — body: { projectPath, endpointId }
|
|
7
|
-
export async function POST(req: Request) {
|
|
8
|
-
const { projectPath, endpointId } = await req.json() as { projectPath: string; endpointId: string };
|
|
9
|
-
if (!projectPath || !endpointId) return NextResponse.json({ error: 'projectPath + endpointId required' }, { status: 400 });
|
|
10
|
-
|
|
11
|
-
const eps = loadEndpoints(projectPath);
|
|
12
|
-
const ep = eps.find(e => e.id === endpointId);
|
|
13
|
-
if (!ep) return NextResponse.json({ error: 'endpoint not found' }, { status: 404 });
|
|
14
|
-
|
|
15
|
-
const config = loadConfig(projectPath);
|
|
16
|
-
const openApi = config.endpointSource.openApiSpec
|
|
17
|
-
? loadOpenApi(projectPath, config.endpointSource.openApiSpec)
|
|
18
|
-
: null;
|
|
19
|
-
const result = await runEndpoint(ep, config, openApi);
|
|
20
|
-
saveRun(projectPath, [result]);
|
|
21
|
-
return NextResponse.json(result);
|
|
22
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { loadConfig, loadEndpoints, saveRun, saveFailures } from '@/lib/migration/store';
|
|
2
|
-
import { runEndpoints } from '@/lib/migration/runner';
|
|
3
|
-
import type { Endpoint, RunResult, Failure } from '@/lib/migration/types';
|
|
4
|
-
|
|
5
|
-
export const dynamic = 'force-dynamic';
|
|
6
|
-
|
|
7
|
-
// POST /api/migration/run-batch — body: { projectPath, endpointIds?, onlyStatus?, concurrency? }
|
|
8
|
-
// Returns SSE stream with progress and final result.
|
|
9
|
-
export async function POST(req: Request) {
|
|
10
|
-
const { projectPath, endpointIds, onlyStatus, concurrency } = await req.json() as {
|
|
11
|
-
projectPath: string;
|
|
12
|
-
endpointIds?: string[];
|
|
13
|
-
onlyStatus?: string[];
|
|
14
|
-
concurrency?: number;
|
|
15
|
-
};
|
|
16
|
-
if (!projectPath) return new Response(JSON.stringify({ error: 'projectPath required' }), { status: 400 });
|
|
17
|
-
|
|
18
|
-
const config = loadConfig(projectPath);
|
|
19
|
-
const all = loadEndpoints(projectPath);
|
|
20
|
-
let toRun: Endpoint[] = all;
|
|
21
|
-
if (endpointIds && endpointIds.length > 0) {
|
|
22
|
-
const ids = new Set(endpointIds);
|
|
23
|
-
toRun = all.filter(e => ids.has(e.id));
|
|
24
|
-
} else if (onlyStatus && onlyStatus.length > 0) {
|
|
25
|
-
const s = new Set(onlyStatus);
|
|
26
|
-
toRun = all.filter(e => s.has(e.status));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const encoder = new TextEncoder();
|
|
30
|
-
const stream = new ReadableStream({
|
|
31
|
-
async start(controller) {
|
|
32
|
-
const send = (event: string, data: any) => {
|
|
33
|
-
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
34
|
-
};
|
|
35
|
-
send('start', { total: toRun.length });
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const results = await runEndpoints(toRun, config, {
|
|
39
|
-
concurrency: concurrency ?? 4,
|
|
40
|
-
projectPath,
|
|
41
|
-
onProgress: (done, total, last) => {
|
|
42
|
-
send('progress', { done, total, result: last });
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
saveRun(projectPath, results);
|
|
46
|
-
|
|
47
|
-
const failures: Failure[] = results
|
|
48
|
-
.filter(r => r.match === 'fail' || r.match === 'error')
|
|
49
|
-
.map(r => {
|
|
50
|
-
const ep = toRun.find(e => e.id === r.endpointId)!;
|
|
51
|
-
return {
|
|
52
|
-
endpointId: r.endpointId,
|
|
53
|
-
controller: ep.controller,
|
|
54
|
-
method: ep.method,
|
|
55
|
-
path: ep.path,
|
|
56
|
-
errorType: r.errorType || 'unknown',
|
|
57
|
-
errorMessage: r.errorMessage || '',
|
|
58
|
-
lastSeenAt: r.startedAt,
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
saveFailures(projectPath, failures);
|
|
62
|
-
|
|
63
|
-
send('done', {
|
|
64
|
-
total: results.length,
|
|
65
|
-
pass: results.filter(r => r.match === 'pass').length,
|
|
66
|
-
fail: results.filter(r => r.match === 'fail').length,
|
|
67
|
-
stubOk: results.filter(r => r.match === 'stub-ok').length,
|
|
68
|
-
error: results.filter(r => r.match === 'error').length,
|
|
69
|
-
failures: failures.length,
|
|
70
|
-
});
|
|
71
|
-
} catch (e: any) {
|
|
72
|
-
send('error', { message: e?.message || String(e) });
|
|
73
|
-
} finally {
|
|
74
|
-
controller.close();
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return new Response(stream, {
|
|
80
|
-
headers: {
|
|
81
|
-
'Content-Type': 'text/event-stream',
|
|
82
|
-
'Cache-Control': 'no-cache',
|
|
83
|
-
'Connection': 'keep-alive',
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
}
|