diff --git a/src/app/api/list-workflows/route.ts b/src/app/api/list-workflows/route.ts index 0f19b9e6..d4084d96 100644 --- a/src/app/api/list-workflows/route.ts +++ b/src/app/api/list-workflows/route.ts @@ -6,10 +6,58 @@ import { validateWorkflowPath } from "@/utils/pathValidation"; interface WorkflowListEntry { name: string; directoryPath: string; - nodeCount: number; lastModified: number; } +// Read just enough of the file to find the "name" field without parsing the whole thing. +// Workflow JSON starts with { "version": 1, "name": "..." ... } so 1KB is plenty. +const HEAD_BYTES = 1024; + +async function probeWorkflow( + dirPath: string, + dirName: string +): Promise { + try { + const files = await fs.readdir(dirPath); + const jsonFiles = files.filter((f) => f.endsWith(".json")); + + for (const jsonFile of jsonFiles) { + const filePath = path.join(dirPath, jsonFile); + try { + const handle = await fs.open(filePath, "r"); + try { + const buf = Buffer.alloc(HEAD_BYTES); + const { bytesRead } = await handle.read(buf, 0, HEAD_BYTES, 0); + const head = buf.toString("utf-8", 0, bytesRead); + + // Quick check: must look like a workflow file + if (!head.includes('"version"') || !head.includes('"nodes"')) { + continue; + } + + // Extract name via regex — avoids JSON.parse on potentially huge files + const nameMatch = head.match(/"name"\s*:\s*"([^"]*)"/); + const name = nameMatch ? nameMatch[1] : dirName; + + const stat = await fs.stat(filePath); + return { + name, + directoryPath: dirPath, + lastModified: stat.mtimeMs, + }; + } finally { + await handle.close(); + } + } catch { + continue; + } + } + } catch { + // Can't read directory + } + return null; +} + export async function GET(request: NextRequest) { const parentPath = request.nextUrl.searchParams.get("path"); @@ -32,39 +80,16 @@ export async function GET(request: NextRequest) { const entries = await fs.readdir(parentPath, { withFileTypes: true }); const directories = entries.filter((e) => e.isDirectory()); - const workflows: WorkflowListEntry[] = []; - - for (const dir of directories) { - const dirPath = path.join(parentPath, dir.name); - try { - const files = await fs.readdir(dirPath); - const jsonFiles = files.filter((f) => f.endsWith(".json")); - - for (const jsonFile of jsonFiles) { - try { - const filePath = path.join(dirPath, jsonFile); - const content = await fs.readFile(filePath, "utf-8"); - const parsed = JSON.parse(content); + // Probe all directories in parallel + const results = await Promise.all( + directories.map((dir) => + probeWorkflow(path.join(parentPath, dir.name), dir.name) + ) + ); - if (parsed.version && parsed.nodes && parsed.edges) { - const stat = await fs.stat(filePath); - workflows.push({ - name: parsed.name || dir.name, - directoryPath: dirPath, - nodeCount: parsed.nodes.length, - lastModified: stat.mtimeMs, - }); - break; // Use first valid workflow file per directory - } - } catch { - continue; - } - } - } catch { - // Skip directories we can't read - continue; - } - } + const workflows = results.filter( + (r): r is WorkflowListEntry => r !== null + ); // Sort by most recently modified first workflows.sort((a, b) => b.lastModified - a.lastModified); @@ -74,7 +99,8 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to list workflows", + error: + error instanceof Error ? error.message : "Failed to list workflows", }, { status: 500 } ); diff --git a/src/components/quickstart/WorkflowBrowserView.tsx b/src/components/quickstart/WorkflowBrowserView.tsx index ee78a98c..0e33b98a 100644 --- a/src/components/quickstart/WorkflowBrowserView.tsx +++ b/src/components/quickstart/WorkflowBrowserView.tsx @@ -11,7 +11,6 @@ import { interface WorkflowListEntry { name: string; directoryPath: string; - nodeCount: number; lastModified: number; } @@ -261,11 +260,7 @@ export function WorkflowBrowserView({ {entry.name} -
- - {entry.nodeCount} node{entry.nodeCount !== 1 ? "s" : ""} - - · +
{formatRelativeTime(entry.lastModified)}