@ryanfw/prompt-orchestration-pipeline 0.10.0 → 0.12.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/package.json +3 -1
- package/src/api/index.js +38 -1
- package/src/components/DAGGrid.jsx +180 -53
- package/src/components/JobDetail.jsx +11 -0
- package/src/components/TaskDetailSidebar.jsx +27 -3
- package/src/components/UploadSeed.jsx +2 -2
- package/src/components/ui/RestartJobModal.jsx +26 -6
- package/src/components/ui/StopJobModal.jsx +183 -0
- package/src/core/config.js +7 -3
- package/src/core/lifecycle-policy.js +62 -0
- package/src/core/orchestrator.js +32 -0
- package/src/core/pipeline-runner.js +312 -217
- package/src/core/status-initializer.js +155 -0
- package/src/core/status-writer.js +235 -13
- package/src/pages/Code.jsx +8 -1
- package/src/pages/PipelineDetail.jsx +85 -3
- package/src/pages/PromptPipelineDashboard.jsx +10 -11
- package/src/ui/client/adapters/job-adapter.js +81 -2
- package/src/ui/client/api.js +233 -8
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +92 -0
- package/src/ui/client/hooks/useJobList.js +14 -1
- package/src/ui/dist/app.js +262 -0
- package/src/ui/dist/assets/{index-DqkbzXZ1.js → index-B320avRx.js} +5051 -2186
- package/src/ui/dist/assets/index-B320avRx.js.map +1 -0
- package/src/ui/dist/assets/style-BYCoLBnK.css +62 -0
- package/src/ui/dist/favicon.svg +12 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/file-endpoints.js +330 -0
- package/src/ui/endpoints/job-control-endpoints.js +1001 -0
- package/src/ui/endpoints/job-endpoints.js +62 -0
- package/src/ui/endpoints/sse-endpoints.js +223 -0
- package/src/ui/endpoints/state-endpoint.js +85 -0
- package/src/ui/endpoints/upload-endpoints.js +406 -0
- package/src/ui/express-app.js +182 -0
- package/src/ui/server.js +38 -1788
- package/src/ui/sse-broadcast.js +93 -0
- package/src/ui/utils/http-utils.js +139 -0
- package/src/ui/utils/mime-types.js +196 -0
- package/src/ui/vite.config.js +22 -0
- package/src/ui/zip-utils.js +103 -0
- package/src/utils/jobs.js +39 -0
- package/src/ui/dist/assets/style-DBF9NQGk.css +0 -62
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE broadcast module for state updates
|
|
3
|
+
* Extracted from router.js to support Express migration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sseRegistry } from "./sse.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decorate change with job ID extracted from file path
|
|
10
|
+
*/
|
|
11
|
+
function decorateChangeWithJobId(change) {
|
|
12
|
+
if (!change || typeof change !== "object") return change;
|
|
13
|
+
const normalizedPath = String(change.path || "").replace(/\\/g, "/");
|
|
14
|
+
const match = normalizedPath.match(
|
|
15
|
+
/pipeline-data\/(current|complete|pending|rejected)\/([^/]+)/
|
|
16
|
+
);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return change;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...change,
|
|
22
|
+
lifecycle: match[1],
|
|
23
|
+
jobId: match[2],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Prioritize job status changes over other changes
|
|
29
|
+
*/
|
|
30
|
+
function prioritizeJobStatusChange(changes = []) {
|
|
31
|
+
const normalized = changes.map((change) => decorateChangeWithJobId(change));
|
|
32
|
+
const statusChange = normalized.find(
|
|
33
|
+
(change) =>
|
|
34
|
+
typeof change?.path === "string" &&
|
|
35
|
+
/tasks-status\.json$/.test(change.path)
|
|
36
|
+
);
|
|
37
|
+
return statusChange || normalized[0] || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Broadcast state update to all SSE clients
|
|
42
|
+
*
|
|
43
|
+
* NOTE: Per plan, SSE should emit compact, incremental events rather than
|
|
44
|
+
* streaming full application state. Use /api/state for full snapshot
|
|
45
|
+
* retrieval on client bootstrap. This function will emit only the most
|
|
46
|
+
* recent change when available (type: "state:change") and fall back to a
|
|
47
|
+
* lightweight summary event if no recent change is present.
|
|
48
|
+
*/
|
|
49
|
+
function broadcastStateUpdate(currentState) {
|
|
50
|
+
try {
|
|
51
|
+
const recentChanges = (currentState && currentState.recentChanges) || [];
|
|
52
|
+
const latest = prioritizeJobStatusChange(recentChanges);
|
|
53
|
+
if (latest) {
|
|
54
|
+
// Emit only the most recent change as a compact, typed event
|
|
55
|
+
const eventData = { type: "state:change", data: latest };
|
|
56
|
+
sseRegistry.broadcast(eventData);
|
|
57
|
+
} else {
|
|
58
|
+
// Fallback: emit a minimal summary so clients can observe a state "tick"
|
|
59
|
+
const eventData = {
|
|
60
|
+
type: "state:summary",
|
|
61
|
+
data: {
|
|
62
|
+
changeCount:
|
|
63
|
+
currentState && currentState.changeCount
|
|
64
|
+
? currentState.changeCount
|
|
65
|
+
: 0,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
sseRegistry.broadcast(eventData);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// Defensive: if something unexpected happens, fall back to a lightweight notification
|
|
72
|
+
try {
|
|
73
|
+
console.error("[Router] Error in broadcastStateUpdate:", err);
|
|
74
|
+
sseRegistry.broadcast({
|
|
75
|
+
type: "state:summary",
|
|
76
|
+
data: {
|
|
77
|
+
changeCount:
|
|
78
|
+
currentState && currentState.changeCount
|
|
79
|
+
? currentState.changeCount
|
|
80
|
+
: 0,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
} catch (fallbackErr) {
|
|
84
|
+
// Log error to aid debugging; this should never happen unless sseRegistry.broadcast is broken
|
|
85
|
+
console.error(
|
|
86
|
+
"Failed to broadcast fallback state summary in broadcastStateUpdate:",
|
|
87
|
+
fallbackErr
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { broadcastStateUpdate };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP utility functions for request/response handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Send JSON response with proper headers
|
|
7
|
+
* @param {http.ServerResponse} res - HTTP response object
|
|
8
|
+
* @param {number} code - HTTP status code
|
|
9
|
+
* @param {Object} obj - Response body object
|
|
10
|
+
*/
|
|
11
|
+
export const sendJson = (res, code, obj) => {
|
|
12
|
+
res.writeHead(code, {
|
|
13
|
+
"content-type": "application/json",
|
|
14
|
+
connection: "close",
|
|
15
|
+
});
|
|
16
|
+
res.end(JSON.stringify(obj));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read raw request body with size limit
|
|
21
|
+
* @param {http.IncomingMessage} req - HTTP request object
|
|
22
|
+
* @param {number} maxBytes - Maximum bytes to read (default: 2MB)
|
|
23
|
+
* @returns {Promise<Buffer>} Raw request body as Buffer
|
|
24
|
+
*/
|
|
25
|
+
export async function readRawBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
26
|
+
// 2MB guard
|
|
27
|
+
const chunks = [];
|
|
28
|
+
let total = 0;
|
|
29
|
+
for await (const chunk of req) {
|
|
30
|
+
total += chunk.length;
|
|
31
|
+
if (total > maxBytes) throw new Error("Payload too large");
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
}
|
|
34
|
+
return Buffer.concat(chunks);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse multipart form data from request
|
|
39
|
+
* @param {http.IncomingMessage} req - HTTP request object
|
|
40
|
+
* @returns {Promise<Object>} Parsed form data with file content as Buffer
|
|
41
|
+
*/
|
|
42
|
+
export function parseMultipartFormData(req) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let boundary = null;
|
|
46
|
+
|
|
47
|
+
// Extract boundary from content-type header
|
|
48
|
+
const contentType = req.headers["content-type"];
|
|
49
|
+
if (!contentType || !contentType.includes("multipart/form-data")) {
|
|
50
|
+
reject(new Error("Invalid content-type: expected multipart/form-data"));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
|
|
55
|
+
if (!boundaryMatch) {
|
|
56
|
+
reject(new Error("Missing boundary in content-type"));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
boundary = `--${boundaryMatch[1].trim()}`;
|
|
61
|
+
|
|
62
|
+
req.on("data", (chunk) => {
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
req.on("end", () => {
|
|
67
|
+
try {
|
|
68
|
+
const buffer = Buffer.concat(chunks);
|
|
69
|
+
|
|
70
|
+
// Find file part in the buffer using string operations for headers
|
|
71
|
+
const data = buffer.toString(
|
|
72
|
+
"utf8",
|
|
73
|
+
0,
|
|
74
|
+
Math.min(buffer.length, 1024 * 1024)
|
|
75
|
+
); // First MB for header search
|
|
76
|
+
const parts = data.split(boundary);
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < parts.length; i++) {
|
|
79
|
+
const part = parts[i];
|
|
80
|
+
|
|
81
|
+
if (part.includes('name="file"') && part.includes("filename")) {
|
|
82
|
+
// Extract filename
|
|
83
|
+
const filenameMatch = part.match(/filename="([^"]+)"/);
|
|
84
|
+
if (!filenameMatch) continue;
|
|
85
|
+
|
|
86
|
+
// Extract content type
|
|
87
|
+
const contentTypeMatch = part.match(/Content-Type:\s*([^\r\n]+)/);
|
|
88
|
+
|
|
89
|
+
// Find this specific part's start in the data string
|
|
90
|
+
const partIndexInData = data.indexOf(part);
|
|
91
|
+
const headerEndInPart = part.indexOf("\r\n\r\n");
|
|
92
|
+
if (headerEndInPart === -1) {
|
|
93
|
+
reject(
|
|
94
|
+
new Error("Could not find end of headers in multipart part")
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate the actual byte positions in the buffer for this part
|
|
100
|
+
const headerEndInData = partIndexInData + headerEndInPart + 4;
|
|
101
|
+
|
|
102
|
+
// Use binary buffer to find the next boundary
|
|
103
|
+
const boundaryBuf = Buffer.from(boundary, "ascii");
|
|
104
|
+
const nextBoundaryIndex = buffer.indexOf(
|
|
105
|
+
boundaryBuf,
|
|
106
|
+
headerEndInData
|
|
107
|
+
);
|
|
108
|
+
const contentEndInData =
|
|
109
|
+
nextBoundaryIndex !== -1
|
|
110
|
+
? nextBoundaryIndex - 2 // Subtract 2 for \r\n before boundary
|
|
111
|
+
: buffer.length;
|
|
112
|
+
|
|
113
|
+
// Extract the file content as Buffer
|
|
114
|
+
const contentBuffer = buffer.slice(
|
|
115
|
+
headerEndInData,
|
|
116
|
+
contentEndInData
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
resolve({
|
|
120
|
+
filename: filenameMatch[1],
|
|
121
|
+
contentType: contentTypeMatch
|
|
122
|
+
? contentTypeMatch[1]
|
|
123
|
+
: "application/octet-stream",
|
|
124
|
+
contentBuffer: contentBuffer,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
reject(new Error("No file field found in form data"));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("Error parsing multipart:", error);
|
|
133
|
+
reject(error);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
req.on("error", reject);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
// MIME type detection map
|
|
4
|
+
const MIME_MAP = {
|
|
5
|
+
// Text types
|
|
6
|
+
".txt": "text/plain",
|
|
7
|
+
".log": "text/plain",
|
|
8
|
+
".md": "text/markdown",
|
|
9
|
+
".csv": "text/csv",
|
|
10
|
+
".json": "application/json",
|
|
11
|
+
".xml": "application/xml",
|
|
12
|
+
".yaml": "application/x-yaml",
|
|
13
|
+
".yml": "application/x-yaml",
|
|
14
|
+
".toml": "application/toml",
|
|
15
|
+
".ini": "text/plain",
|
|
16
|
+
".conf": "text/plain",
|
|
17
|
+
".config": "text/plain",
|
|
18
|
+
".env": "text/plain",
|
|
19
|
+
".gitignore": "text/plain",
|
|
20
|
+
".dockerfile": "text/plain",
|
|
21
|
+
".sh": "application/x-sh",
|
|
22
|
+
".bash": "application/x-sh",
|
|
23
|
+
".zsh": "application/x-sh",
|
|
24
|
+
".fish": "application/x-fish",
|
|
25
|
+
".ps1": "application/x-powershell",
|
|
26
|
+
".bat": "application/x-bat",
|
|
27
|
+
".cmd": "application/x-cmd",
|
|
28
|
+
|
|
29
|
+
// Code types
|
|
30
|
+
".js": "application/javascript",
|
|
31
|
+
".mjs": "application/javascript",
|
|
32
|
+
".cjs": "application/javascript",
|
|
33
|
+
".ts": "application/typescript",
|
|
34
|
+
".mts": "application/typescript",
|
|
35
|
+
".cts": "application/typescript",
|
|
36
|
+
".jsx": "application/javascript",
|
|
37
|
+
".tsx": "application/typescript",
|
|
38
|
+
".py": "text/x-python",
|
|
39
|
+
".rb": "text/x-ruby",
|
|
40
|
+
".php": "application/x-php",
|
|
41
|
+
".java": "text/x-java-source",
|
|
42
|
+
".c": "text/x-c",
|
|
43
|
+
".cpp": "text/x-c++",
|
|
44
|
+
".cc": "text/x-c++",
|
|
45
|
+
".cxx": "text/x-c++",
|
|
46
|
+
".h": "text/x-c",
|
|
47
|
+
".hpp": "text/x-c++",
|
|
48
|
+
".cs": "text/x-csharp",
|
|
49
|
+
".go": "text/x-go",
|
|
50
|
+
".rs": "text/x-rust",
|
|
51
|
+
".swift": "text/x-swift",
|
|
52
|
+
".kt": "text/x-kotlin",
|
|
53
|
+
".scala": "text/x-scala",
|
|
54
|
+
".r": "text/x-r",
|
|
55
|
+
".sql": "application/sql",
|
|
56
|
+
".pl": "text/x-perl",
|
|
57
|
+
".lua": "text/x-lua",
|
|
58
|
+
".vim": "text/x-vim",
|
|
59
|
+
".el": "text/x-elisp",
|
|
60
|
+
".lisp": "text/x-lisp",
|
|
61
|
+
".hs": "text/x-haskell",
|
|
62
|
+
".ml": "text/x-ocaml",
|
|
63
|
+
".ex": "text/x-elixir",
|
|
64
|
+
".exs": "text/x-elixir",
|
|
65
|
+
".erl": "text/x-erlang",
|
|
66
|
+
".beam": "application/x-erlang-beam",
|
|
67
|
+
|
|
68
|
+
// Web types
|
|
69
|
+
".html": "text/html",
|
|
70
|
+
".htm": "text/html",
|
|
71
|
+
".xhtml": "application/xhtml+xml",
|
|
72
|
+
".css": "text/css",
|
|
73
|
+
".scss": "text/x-scss",
|
|
74
|
+
".sass": "text/x-sass",
|
|
75
|
+
".less": "text/x-less",
|
|
76
|
+
".styl": "text/x-stylus",
|
|
77
|
+
".vue": "text/x-vue",
|
|
78
|
+
".svelte": "text/x-svelte",
|
|
79
|
+
|
|
80
|
+
// Data formats
|
|
81
|
+
".pdf": "application/pdf",
|
|
82
|
+
".doc": "application/msword",
|
|
83
|
+
".docx":
|
|
84
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
85
|
+
".xls": "application/vnd.ms-excel",
|
|
86
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
87
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
88
|
+
".pptx":
|
|
89
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
90
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
91
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
92
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
93
|
+
|
|
94
|
+
// Images
|
|
95
|
+
".png": "image/png",
|
|
96
|
+
".jpg": "image/jpeg",
|
|
97
|
+
".jpeg": "image/jpeg",
|
|
98
|
+
".gif": "image/gif",
|
|
99
|
+
".bmp": "image/bmp",
|
|
100
|
+
".webp": "image/webp",
|
|
101
|
+
".svg": "image/svg+xml",
|
|
102
|
+
".ico": "image/x-icon",
|
|
103
|
+
".tiff": "image/tiff",
|
|
104
|
+
".tif": "image/tiff",
|
|
105
|
+
".psd": "image/vnd.adobe.photoshop",
|
|
106
|
+
".ai": "application/pdf", // Illustrator files often saved as PDF
|
|
107
|
+
".eps": "application/postscript",
|
|
108
|
+
|
|
109
|
+
// Audio
|
|
110
|
+
".mp3": "audio/mpeg",
|
|
111
|
+
".wav": "audio/wav",
|
|
112
|
+
".ogg": "audio/ogg",
|
|
113
|
+
".flac": "audio/flac",
|
|
114
|
+
".aac": "audio/aac",
|
|
115
|
+
".m4a": "audio/mp4",
|
|
116
|
+
".wma": "audio/x-ms-wma",
|
|
117
|
+
|
|
118
|
+
// Video
|
|
119
|
+
".mp4": "video/mp4",
|
|
120
|
+
".avi": "video/x-msvideo",
|
|
121
|
+
".mov": "video/quicktime",
|
|
122
|
+
".wmv": "video/x-ms-wmv",
|
|
123
|
+
".flv": "video/x-flv",
|
|
124
|
+
".webm": "video/webm",
|
|
125
|
+
".mkv": "video/x-matroska",
|
|
126
|
+
".m4v": "video/mp4",
|
|
127
|
+
|
|
128
|
+
// Archives
|
|
129
|
+
".zip": "application/zip",
|
|
130
|
+
".rar": "application/x-rar-compressed",
|
|
131
|
+
".tar": "application/x-tar",
|
|
132
|
+
".gz": "application/gzip",
|
|
133
|
+
".tgz": "application/gzip",
|
|
134
|
+
".bz2": "application/x-bzip2",
|
|
135
|
+
".xz": "application/x-xz",
|
|
136
|
+
".7z": "application/x-7z-compressed",
|
|
137
|
+
".deb": "application/x-debian-package",
|
|
138
|
+
".rpm": "application/x-rpm",
|
|
139
|
+
".dmg": "application/x-apple-diskimage",
|
|
140
|
+
".iso": "application/x-iso9660-image",
|
|
141
|
+
|
|
142
|
+
// Fonts
|
|
143
|
+
".ttf": "font/ttf",
|
|
144
|
+
".otf": "font/otf",
|
|
145
|
+
".woff": "font/woff",
|
|
146
|
+
".woff2": "font/woff2",
|
|
147
|
+
".eot": "application/vnd.ms-fontobject",
|
|
148
|
+
|
|
149
|
+
// Misc
|
|
150
|
+
".bin": "application/octet-stream",
|
|
151
|
+
".exe": "application/x-msdownload",
|
|
152
|
+
".dll": "application/x-msdownload",
|
|
153
|
+
".so": "application/x-sharedlib",
|
|
154
|
+
".dylib": "application/x-mach-binary",
|
|
155
|
+
".class": "application/java-vm",
|
|
156
|
+
".jar": "application/java-archive",
|
|
157
|
+
".war": "application/java-archive",
|
|
158
|
+
".ear": "application/java-archive",
|
|
159
|
+
".apk": "application/vnd.android.package-archive",
|
|
160
|
+
".ipa": "application/x-itunes-ipa",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Determine MIME type from file extension
|
|
165
|
+
* @param {string} filename - File name
|
|
166
|
+
* @returns {string} MIME type
|
|
167
|
+
*/
|
|
168
|
+
function getMimeType(filename) {
|
|
169
|
+
const ext = path.extname(filename).toLowerCase();
|
|
170
|
+
return MIME_MAP[ext] || "application/octet-stream";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if MIME type should be treated as text
|
|
175
|
+
* @param {string} mime - MIME type
|
|
176
|
+
* @returns {boolean} True if text-like
|
|
177
|
+
*/
|
|
178
|
+
function isTextMime(mime) {
|
|
179
|
+
return (
|
|
180
|
+
mime.startsWith("text/") ||
|
|
181
|
+
mime === "application/json" ||
|
|
182
|
+
mime === "application/javascript" ||
|
|
183
|
+
mime === "application/xml" ||
|
|
184
|
+
mime === "application/x-yaml" ||
|
|
185
|
+
mime === "application/x-sh" ||
|
|
186
|
+
mime === "application/x-bat" ||
|
|
187
|
+
mime === "application/x-cmd" ||
|
|
188
|
+
mime === "application/x-powershell" ||
|
|
189
|
+
mime === "image/svg+xml" ||
|
|
190
|
+
mime === "application/x-ndjson" ||
|
|
191
|
+
mime === "text/csv" ||
|
|
192
|
+
mime === "text/markdown"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { MIME_MAP, getMimeType, isTextMime };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
|
|
4
|
+
// Fix the publicDir path to resolve the static assets issue.
|
|
5
|
+
// Vite was looking in src/ui/client/public (relative to root),
|
|
6
|
+
// but public assets are in src/ui/public.
|
|
7
|
+
// By setting publicDir to an absolute path, Vite will copy
|
|
8
|
+
// contents of src/ui/public to dist during build.
|
|
9
|
+
const publicDir = new URL("./public", import.meta.url).pathname;
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
plugins: [react()],
|
|
13
|
+
root: "src/ui/client",
|
|
14
|
+
build: {
|
|
15
|
+
outDir: "../../dist", // Output to src/ui/dist
|
|
16
|
+
sourcemap: true,
|
|
17
|
+
},
|
|
18
|
+
publicDir,
|
|
19
|
+
server: {
|
|
20
|
+
cors: true,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { unzipSync } from "fflate";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract seed JSON and artifacts from a zip buffer using fflate
|
|
6
|
+
* @param {Buffer|Uint8Array} zipBuffer - Buffer containing zip data
|
|
7
|
+
* @returns {Promise<{seedObject: Object, artifacts: Array<{filename: string, content: Buffer}>}>}
|
|
8
|
+
*/
|
|
9
|
+
export async function extractSeedZip(zipBuffer) {
|
|
10
|
+
// Normalize to Uint8Array for fflate
|
|
11
|
+
const zipData = Buffer.isBuffer(zipBuffer)
|
|
12
|
+
? new Uint8Array(zipBuffer)
|
|
13
|
+
: zipBuffer;
|
|
14
|
+
|
|
15
|
+
console.log("[ZIP] Starting real zip parsing", {
|
|
16
|
+
bufferSize: zipData.length,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Check if this looks like a valid zip by looking for PK signature
|
|
21
|
+
if (zipData.length < 4 || zipData[0] !== 0x50 || zipData[1] !== 0x4b) {
|
|
22
|
+
throw new Error("Invalid ZIP file signature");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Use fflate to extract all entries
|
|
26
|
+
const entries = unzipSync(zipData);
|
|
27
|
+
const artifacts = [];
|
|
28
|
+
let seedObject = null;
|
|
29
|
+
let seedJsonCount = 0;
|
|
30
|
+
|
|
31
|
+
console.log("[ZIP] Extracted entries from zip", {
|
|
32
|
+
entryCount: Object.keys(entries).length,
|
|
33
|
+
entryNames: Object.keys(entries),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Process each entry
|
|
37
|
+
for (const [entryName, rawContent] of Object.entries(entries)) {
|
|
38
|
+
// Skip directory entries (names ending with /)
|
|
39
|
+
if (entryName.endsWith("/")) {
|
|
40
|
+
console.log("[ZIP] Skipping directory entry", { entryName });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Derive filename using basename (flatten directory structure)
|
|
45
|
+
const filename = path.basename(entryName);
|
|
46
|
+
console.log("[ZIP] Processing entry", { entryName, filename });
|
|
47
|
+
|
|
48
|
+
// Convert Uint8Array to Buffer
|
|
49
|
+
const content = Buffer.from(rawContent);
|
|
50
|
+
|
|
51
|
+
// Add to artifacts
|
|
52
|
+
artifacts.push({ filename, content });
|
|
53
|
+
|
|
54
|
+
// Check if this is seed.json
|
|
55
|
+
if (filename === "seed.json") {
|
|
56
|
+
seedJsonCount++;
|
|
57
|
+
try {
|
|
58
|
+
const jsonContent = content.toString("utf8");
|
|
59
|
+
seedObject = JSON.parse(jsonContent);
|
|
60
|
+
console.log("[ZIP] Successfully parsed seed.json", {
|
|
61
|
+
seedName: seedObject.name,
|
|
62
|
+
seedPipeline: seedObject.pipeline,
|
|
63
|
+
});
|
|
64
|
+
} catch (parseError) {
|
|
65
|
+
console.error("[ZIP] Failed to parse seed.json", {
|
|
66
|
+
error: parseError.message,
|
|
67
|
+
filename,
|
|
68
|
+
});
|
|
69
|
+
throw new Error("Invalid JSON");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate that we found at least one seed.json
|
|
75
|
+
if (seedJsonCount === 0) {
|
|
76
|
+
throw new Error("seed.json not found in zip");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (seedJsonCount > 1) {
|
|
80
|
+
console.log(
|
|
81
|
+
"[ZIP] Warning: multiple seed.json files found, using last one",
|
|
82
|
+
{
|
|
83
|
+
count: seedJsonCount,
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log("[ZIP] Zip extraction completed", {
|
|
89
|
+
artifactCount: artifacts.length,
|
|
90
|
+
artifactNames: artifacts.map((a) => a.filename),
|
|
91
|
+
seedKeys: seedObject ? Object.keys(seedObject) : [],
|
|
92
|
+
seedJsonCount,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { seedObject, artifacts };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error("[ZIP] Zip extraction failed", {
|
|
98
|
+
error: error.message,
|
|
99
|
+
bufferSize: zipData.length,
|
|
100
|
+
});
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/utils/jobs.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeTaskState } from "../config/statuses.js";
|
|
2
|
+
|
|
1
3
|
export const countCompleted = (job) => {
|
|
2
4
|
const list = Array.isArray(job?.tasks)
|
|
3
5
|
? job.tasks
|
|
@@ -5,3 +7,40 @@ export const countCompleted = (job) => {
|
|
|
5
7
|
return list.filter((t) => t?.state === "done" || t?.state === "completed")
|
|
6
8
|
.length;
|
|
7
9
|
};
|
|
10
|
+
|
|
11
|
+
export const DisplayCategory = Object.freeze({
|
|
12
|
+
ERRORS: "errors",
|
|
13
|
+
CURRENT: "current",
|
|
14
|
+
COMPLETE: "complete",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function classifyJobForDisplay(job) {
|
|
18
|
+
if (!job) return DisplayCategory.CURRENT;
|
|
19
|
+
|
|
20
|
+
const tasks = Array.isArray(job?.tasks)
|
|
21
|
+
? job.tasks
|
|
22
|
+
: Object.values(job?.tasks || {});
|
|
23
|
+
|
|
24
|
+
const normalizedStates = tasks.map((task) => normalizeTaskState(task?.state));
|
|
25
|
+
|
|
26
|
+
// Precedence: errors > current > complete > fallback to current
|
|
27
|
+
if (
|
|
28
|
+
job.status === "failed" ||
|
|
29
|
+
normalizedStates.some((state) => state === "failed")
|
|
30
|
+
) {
|
|
31
|
+
return DisplayCategory.ERRORS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
job.status === "running" ||
|
|
36
|
+
normalizedStates.some((state) => state === "running")
|
|
37
|
+
) {
|
|
38
|
+
return DisplayCategory.CURRENT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (tasks.length > 0 && normalizedStates.every((state) => state === "done")) {
|
|
42
|
+
return DisplayCategory.COMPLETE;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return DisplayCategory.CURRENT;
|
|
46
|
+
}
|