@aion0/forge 0.5.48 → 0.5.50
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 +50 -4
- package/app/api/craft-system/build/route.ts +78 -0
- package/app/api/craft-system/delete/route.ts +28 -0
- package/app/api/craft-system/helpers/file/route.ts +20 -0
- package/app/api/craft-system/helpers/openapi/route.ts +27 -0
- package/app/api/craft-system/helpers/shell/route.ts +26 -0
- package/app/api/craft-system/inject/route.ts +41 -0
- package/app/api/craft-system/kill-session/route.ts +19 -0
- package/app/api/craft-system/manifest/route.ts +71 -0
- package/app/api/craft-system/marketplace/install/route.ts +11 -0
- package/app/api/craft-system/marketplace/route.ts +18 -0
- package/app/api/craft-system/marketplace/uninstall/route.ts +11 -0
- package/app/api/craft-system/marketplace/update/route.ts +10 -0
- package/app/api/craft-system/marketplace/updates/route.ts +17 -0
- package/app/api/craft-system/publish/auto/route.ts +173 -0
- package/app/api/craft-system/publish/route.ts +50 -0
- package/app/api/craft-system/registry/route.ts +16 -0
- package/app/api/craft-system/runtime/react/route.ts +26 -0
- package/app/api/craft-system/runtime/react-jsx/route.ts +11 -0
- package/app/api/craft-system/runtime/sdk/route.ts +18 -0
- package/app/api/craft-system/scaffold/route.ts +164 -0
- package/app/api/craft-system/sessions/route.ts +45 -0
- package/app/api/craft-system/storage/route.ts +44 -0
- package/app/api/craft-system/tmux-sessions/route.ts +62 -0
- package/app/api/craft-system/ui/route.ts +30 -0
- package/app/api/crafts/[name]/[...route]/route.ts +48 -0
- package/app/api/crafts/route.ts +29 -0
- 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/CraftBuilder.tsx +241 -0
- package/components/CraftManifestEditor.tsx +258 -0
- package/components/CraftMarketplaceModal.tsx +207 -0
- package/components/CraftPublishModal.tsx +285 -0
- package/components/CraftTabs.tsx +279 -0
- package/components/CraftTerminal.tsx +305 -0
- package/components/CraftTerminalPicker.tsx +179 -0
- package/components/CraftsDropdown.tsx +186 -0
- package/components/CraftsMarketplacePanel.tsx +194 -0
- package/components/ProjectDetail.tsx +102 -13
- package/components/SkillsPanel.tsx +12 -4
- package/components/TaskDetail.tsx +250 -52
- package/lib/craft-sdk/client.tsx +260 -0
- package/lib/craft-sdk/server.ts +14 -0
- package/lib/crafts/loader.ts +117 -0
- package/lib/crafts/registry.ts +272 -0
- package/lib/crafts/runtime.ts +208 -0
- package/lib/crafts/types.ts +92 -0
- package/lib/forge-skills/craft-builder.md +231 -0
- package/lib/help-docs/15-crafts.md +127 -0
- package/lib/help-docs/CLAUDE.md +2 -2
- package/lib/task-manager.ts +110 -0
- package/lib/terminal-standalone.ts +1 -0
- package/next.config.ts +1 -1
- package/package.json +2 -1
- package/src/types/index.ts +7 -0
- package/tsconfig.json +6 -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
package/lib/task-manager.ts
CHANGED
|
@@ -119,6 +119,116 @@ export function listTasks(status?: TaskStatus): Task[] {
|
|
|
119
119
|
return rows.map(rowToTask);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Slim list — omits the heavy fields (log / git_diff / result_summary) so the
|
|
123
|
+
// task list view doesn't pull megabytes of JSON for tasks with long sessions.
|
|
124
|
+
// Callers that need the full body should fetch /api/tasks/[id] on demand.
|
|
125
|
+
export function listTasksLite(status?: TaskStatus): Task[] {
|
|
126
|
+
const SLIM_COLS = `
|
|
127
|
+
id, project_name, project_path, prompt, mode, status, priority,
|
|
128
|
+
conversation_id, watch_config, git_branch, cost_usd, error, agent,
|
|
129
|
+
created_at, started_at, completed_at, scheduled_at,
|
|
130
|
+
length(log) AS log_size,
|
|
131
|
+
CASE WHEN result_summary IS NULL THEN NULL ELSE substr(result_summary, 1, 1024) END AS result_summary,
|
|
132
|
+
CASE WHEN git_diff IS NULL THEN 0 ELSE 1 END AS has_git_diff
|
|
133
|
+
`;
|
|
134
|
+
let query = `SELECT ${SLIM_COLS} FROM tasks`;
|
|
135
|
+
const params: string[] = [];
|
|
136
|
+
if (status) {
|
|
137
|
+
query += ' WHERE status = ?';
|
|
138
|
+
params.push(status);
|
|
139
|
+
}
|
|
140
|
+
query += ' ORDER BY created_at DESC';
|
|
141
|
+
const rows = db().prepare(query).all(...params) as any[];
|
|
142
|
+
return rows.map(r => rowToLiteTask(r));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Slice the log without parsing the whole JSON in JS — sqlite JSON1's
|
|
146
|
+
// json_each walks the array and we LIMIT/OFFSET in SQL. For a 1.6 MB log
|
|
147
|
+
// with 238 entries this returns just the requested ~100 in milliseconds.
|
|
148
|
+
export function getTaskLogSlice(id: string, opts: { offset?: number; limit?: number; truncate?: number } = {}):
|
|
149
|
+
{ entries: (TaskLogEntry & { _truncated?: number; _index?: number })[]; total: number } {
|
|
150
|
+
const totalRow = db().prepare(
|
|
151
|
+
`SELECT COALESCE(json_array_length(log), 0) AS n FROM tasks WHERE id = ?`
|
|
152
|
+
).get(id) as { n: number } | undefined;
|
|
153
|
+
const total = totalRow?.n ?? 0;
|
|
154
|
+
if (total === 0) return { entries: [], total: 0 };
|
|
155
|
+
|
|
156
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 200, 2000));
|
|
157
|
+
const offset = Math.max(0, opts.offset ?? Math.max(0, total - limit));
|
|
158
|
+
const truncate = opts.truncate ?? 8192; // per-entry content cap; 0 = no cap
|
|
159
|
+
const rows = db().prepare(
|
|
160
|
+
`SELECT json_each.key AS idx, value FROM tasks, json_each(tasks.log)
|
|
161
|
+
WHERE tasks.id = ?
|
|
162
|
+
ORDER BY json_each.key
|
|
163
|
+
LIMIT ? OFFSET ?`
|
|
164
|
+
).all(id, limit, offset) as { idx: number; value: string }[];
|
|
165
|
+
|
|
166
|
+
const entries = rows.map(r => {
|
|
167
|
+
let entry: TaskLogEntry & { _truncated?: number; _index?: number };
|
|
168
|
+
try { entry = JSON.parse(r.value); }
|
|
169
|
+
catch { entry = { type: 'system', content: '<unparseable entry>' } as any; }
|
|
170
|
+
entry._index = r.idx;
|
|
171
|
+
if (truncate > 0 && typeof entry.content === 'string' && entry.content.length > truncate) {
|
|
172
|
+
const fullLen = entry.content.length;
|
|
173
|
+
entry.content = entry.content.slice(0, truncate);
|
|
174
|
+
entry._truncated = fullLen;
|
|
175
|
+
}
|
|
176
|
+
return entry;
|
|
177
|
+
});
|
|
178
|
+
return { entries, total };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Single entry by index — used to "show full" a previously truncated entry.
|
|
182
|
+
export function getTaskLogEntry(id: string, index: number): TaskLogEntry | null {
|
|
183
|
+
const row = db().prepare(
|
|
184
|
+
`SELECT value FROM tasks, json_each(tasks.log)
|
|
185
|
+
WHERE tasks.id = ? AND json_each.key = ?`
|
|
186
|
+
).get(id, index) as { value: string } | undefined;
|
|
187
|
+
if (!row) return null;
|
|
188
|
+
try { return JSON.parse(row.value); } catch { return null; }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fetch only the heavy fields by id (used when the client needs them after
|
|
192
|
+
// having gotten the lite list earlier).
|
|
193
|
+
export function getTaskBody(id: string): { resultSummary?: string; gitDiff?: string; error?: string } | null {
|
|
194
|
+
const row = db().prepare(
|
|
195
|
+
`SELECT result_summary, git_diff, error FROM tasks WHERE id = ?`
|
|
196
|
+
).get(id) as any;
|
|
197
|
+
if (!row) return null;
|
|
198
|
+
return {
|
|
199
|
+
resultSummary: row.result_summary || undefined,
|
|
200
|
+
gitDiff: row.git_diff || undefined,
|
|
201
|
+
error: row.error || undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function rowToLiteTask(row: any): Task {
|
|
206
|
+
return {
|
|
207
|
+
id: row.id,
|
|
208
|
+
projectName: row.project_name,
|
|
209
|
+
projectPath: row.project_path,
|
|
210
|
+
prompt: row.prompt,
|
|
211
|
+
mode: row.mode || 'prompt',
|
|
212
|
+
status: row.status,
|
|
213
|
+
priority: row.priority,
|
|
214
|
+
conversationId: row.conversation_id || undefined,
|
|
215
|
+
watchConfig: row.watch_config ? JSON.parse(row.watch_config) : undefined,
|
|
216
|
+
log: [], // slim — fetch detail separately
|
|
217
|
+
resultSummary: row.result_summary || undefined, // first 1KB only
|
|
218
|
+
gitDiff: undefined, // not loaded
|
|
219
|
+
gitBranch: row.git_branch || undefined,
|
|
220
|
+
costUSD: row.cost_usd || undefined,
|
|
221
|
+
error: row.error || undefined,
|
|
222
|
+
createdAt: toIsoUTC(row.created_at) ?? row.created_at,
|
|
223
|
+
startedAt: toIsoUTC(row.started_at) ?? undefined,
|
|
224
|
+
completedAt: toIsoUTC(row.completed_at) ?? undefined,
|
|
225
|
+
scheduledAt: toIsoUTC(row.scheduled_at) ?? undefined,
|
|
226
|
+
agent: row.agent || undefined,
|
|
227
|
+
logSize: row.log_size || 0,
|
|
228
|
+
hasGitDiff: !!row.has_git_diff,
|
|
229
|
+
} as Task;
|
|
230
|
+
}
|
|
231
|
+
|
|
122
232
|
export function cancelTask(id: string): boolean {
|
|
123
233
|
const task = getTask(id);
|
|
124
234
|
if (!task) return false;
|
|
@@ -209,6 +209,7 @@ function cleanupOrphanedSessions() {
|
|
|
209
209
|
for (const s of sessions) {
|
|
210
210
|
if (s.attached) continue;
|
|
211
211
|
if (s.name.startsWith(`${SESSION_PREFIX}forge-`)) continue; // workspace agent session — managed by orchestrator
|
|
212
|
+
if (s.name.startsWith('mw-craft-')) continue; // craft session — managed by craft loader
|
|
212
213
|
if (knownSessions.has(s.name)) continue; // saved in terminal state — preserve
|
|
213
214
|
const clients = sessionClients.get(s.name)?.size ?? 0;
|
|
214
215
|
if (clients === 0) {
|
package/next.config.ts
CHANGED
|
@@ -10,7 +10,7 @@ const localIPs = Object.values(networkInterfaces())
|
|
|
10
10
|
.map(i => i!.address);
|
|
11
11
|
|
|
12
12
|
const nextConfig: NextConfig = {
|
|
13
|
-
serverExternalPackages: ['better-sqlite3'],
|
|
13
|
+
serverExternalPackages: ['better-sqlite3', 'esbuild'],
|
|
14
14
|
allowedDevOrigins: localIPs,
|
|
15
15
|
async rewrites() {
|
|
16
16
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aion0/forge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.50",
|
|
4
4
|
"description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@xyflow/react": "^12.10.1",
|
|
42
42
|
"ai": "^6.0.116",
|
|
43
43
|
"better-sqlite3": "^12.6.2",
|
|
44
|
+
"esbuild": "^0.27.3",
|
|
44
45
|
"next": "^16.2.1",
|
|
45
46
|
"next-auth": "5.0.0-beta.30",
|
|
46
47
|
"node-pty": "1.0.0",
|
package/src/types/index.ts
CHANGED
|
@@ -109,6 +109,10 @@ export interface Task {
|
|
|
109
109
|
startedAt?: string;
|
|
110
110
|
completedAt?: string;
|
|
111
111
|
scheduledAt?: string;
|
|
112
|
+
agent?: string;
|
|
113
|
+
// Lite-list metadata: present in /api/tasks responses, undefined in detail
|
|
114
|
+
logSize?: number;
|
|
115
|
+
hasGitDiff?: boolean;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
export interface TaskLogEntry {
|
|
@@ -117,6 +121,9 @@ export interface TaskLogEntry {
|
|
|
117
121
|
content: string;
|
|
118
122
|
tool?: string;
|
|
119
123
|
timestamp: string;
|
|
124
|
+
// Slice-API metadata (server adds these when serving a truncated chunk)
|
|
125
|
+
_index?: number;
|
|
126
|
+
_truncated?: number; // original content.length before truncation
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
export interface AppConfig {
|
package/tsconfig.json
CHANGED
|
@@ -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
|
-
}
|