@ryanfw/prompt-orchestration-pipeline 0.9.1 → 0.11.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 +2 -1
- package/src/api/index.js +38 -1
- package/src/components/DAGGrid.jsx +24 -7
- package/src/components/JobDetail.jsx +11 -0
- package/src/components/TaskDetailSidebar.jsx +27 -3
- package/src/components/UploadSeed.jsx +2 -2
- package/src/config/log-events.js +77 -0
- package/src/core/file-io.js +202 -7
- package/src/core/orchestrator.js +140 -4
- package/src/core/pipeline-runner.js +84 -6
- package/src/core/status-initializer.js +155 -0
- package/src/core/status-writer.js +151 -13
- package/src/core/symlink-utils.js +196 -0
- package/src/core/task-runner.js +37 -7
- package/src/ui/client/adapters/job-adapter.js +21 -2
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +92 -0
- package/src/ui/dist/assets/{index-DqkbzXZ1.js → index-DeDzq-Kk.js} +129 -14
- package/src/ui/dist/assets/style-aBtD_Yrs.css +62 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/server.js +201 -109
- package/src/ui/zip-utils.js +103 -0
- package/src/ui/dist/assets/style-DBF9NQGk.css +0 -62
package/src/ui/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
/>
|
|
12
12
|
<title>Prompt Pipeline Dashboard</title>
|
|
13
13
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/style-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DeDzq-Kk.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/style-aBtD_Yrs.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/src/ui/server.js
CHANGED
|
@@ -12,7 +12,10 @@ import * as state from "./state.js";
|
|
|
12
12
|
// Import orchestrator-related functions only in non-test mode
|
|
13
13
|
let submitJobWithValidation;
|
|
14
14
|
import { sseRegistry } from "./sse.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
resetJobToCleanSlate,
|
|
17
|
+
initializeJobArtifacts,
|
|
18
|
+
} from "../core/status-writer.js";
|
|
16
19
|
import { spawn } from "node:child_process";
|
|
17
20
|
import {
|
|
18
21
|
getPendingSeedPath,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
} from "../config/paths.js";
|
|
24
27
|
import { handleJobList, handleJobDetail } from "./endpoints/job-endpoints.js";
|
|
25
28
|
import { generateJobId } from "../utils/id-generator.js";
|
|
29
|
+
import { extractSeedZip } from "./zip-utils.js";
|
|
26
30
|
|
|
27
31
|
// Get __dirname equivalent in ES modules
|
|
28
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -102,9 +106,14 @@ function hasValidPayload(seed) {
|
|
|
102
106
|
* Handle seed upload directly without starting orchestrator (for test environment)
|
|
103
107
|
* @param {Object} seedObject - Seed object to upload
|
|
104
108
|
* @param {string} dataDir - Base data directory
|
|
109
|
+
* @param {Array} uploadArtifacts - Array of {filename, content} objects
|
|
105
110
|
* @returns {Promise<Object>} Result object
|
|
106
111
|
*/
|
|
107
|
-
async function handleSeedUploadDirect(
|
|
112
|
+
async function handleSeedUploadDirect(
|
|
113
|
+
seedObject,
|
|
114
|
+
dataDir,
|
|
115
|
+
uploadArtifacts = []
|
|
116
|
+
) {
|
|
108
117
|
let partialFiles = [];
|
|
109
118
|
|
|
110
119
|
try {
|
|
@@ -124,16 +133,39 @@ async function handleSeedUploadDirect(seedObject, dataDir) {
|
|
|
124
133
|
return { success: false, message: "Required fields missing" };
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
// Validate name format
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
// Validate name format using the same logic as seed validator
|
|
137
|
+
if (
|
|
138
|
+
!seedObject.name ||
|
|
139
|
+
typeof seedObject.name !== "string" ||
|
|
140
|
+
seedObject.name.trim() === ""
|
|
141
|
+
) {
|
|
130
142
|
return {
|
|
131
143
|
success: false,
|
|
132
|
-
message:
|
|
133
|
-
"name must contain only alphanumeric characters, hyphens, and underscores",
|
|
144
|
+
message: "name field is required",
|
|
134
145
|
};
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
const trimmedName = seedObject.name.trim();
|
|
149
|
+
if (trimmedName.length > 120) {
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
message: "name must be 120 characters or less",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Allow spaces and common punctuation for better UX
|
|
157
|
+
// Still disallow control characters and path traversal patterns
|
|
158
|
+
const dangerousPattern = /[\x00-\x1f\x7f-\x9f]/;
|
|
159
|
+
if (dangerousPattern.test(trimmedName)) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
message: "name must contain only printable characters",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Update seedObject with validated trimmed name
|
|
167
|
+
seedObject.name = trimmedName;
|
|
168
|
+
|
|
137
169
|
// Generate a random job ID
|
|
138
170
|
const jobId = generateJobId();
|
|
139
171
|
|
|
@@ -197,6 +229,16 @@ async function handleSeedUploadDirect(seedObject, dataDir) {
|
|
|
197
229
|
JSON.stringify(pipelineSnapshot, null, 2)
|
|
198
230
|
);
|
|
199
231
|
|
|
232
|
+
// Initialize job artifacts if any provided
|
|
233
|
+
if (uploadArtifacts.length > 0) {
|
|
234
|
+
try {
|
|
235
|
+
await initializeJobArtifacts(currentJobDir, uploadArtifacts);
|
|
236
|
+
} catch (artifactError) {
|
|
237
|
+
// Don't fail the upload if artifact initialization fails, just log the error
|
|
238
|
+
console.error("Failed to initialize job artifacts:", artifactError);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
return {
|
|
201
243
|
success: true,
|
|
202
244
|
jobId,
|
|
@@ -298,6 +340,82 @@ function prioritizeJobStatusChange(changes = []) {
|
|
|
298
340
|
return statusChange || normalized[0] || null;
|
|
299
341
|
}
|
|
300
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Normalize seed upload from various input formats
|
|
345
|
+
* @param {http.IncomingMessage} req - HTTP request
|
|
346
|
+
* @param {string} contentTypeHeader - Content-Type header
|
|
347
|
+
* @returns {Promise<{seedObject: Object, uploadArtifacts: Array<{filename: string, content: Buffer}>}>}
|
|
348
|
+
*/
|
|
349
|
+
async function normalizeSeedUpload({ req, contentTypeHeader }) {
|
|
350
|
+
// Handle application/json uploads
|
|
351
|
+
if (contentTypeHeader.includes("application/json")) {
|
|
352
|
+
const buffer = await readRawBody(req);
|
|
353
|
+
try {
|
|
354
|
+
const seedObject = JSON.parse(buffer.toString("utf8") || "{}");
|
|
355
|
+
return {
|
|
356
|
+
seedObject,
|
|
357
|
+
uploadArtifacts: [{ filename: "seed.json", content: buffer }],
|
|
358
|
+
};
|
|
359
|
+
} catch (error) {
|
|
360
|
+
throw new Error("Invalid JSON");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Handle multipart form data uploads
|
|
365
|
+
const formData = await parseMultipartFormData(req);
|
|
366
|
+
if (!formData.contentBuffer) {
|
|
367
|
+
throw new Error("No file content found");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check if this is a zip file
|
|
371
|
+
const isZipFile =
|
|
372
|
+
formData.contentType === "application/zip" ||
|
|
373
|
+
formData.filename?.toLowerCase().endsWith(".zip");
|
|
374
|
+
|
|
375
|
+
if (isZipFile) {
|
|
376
|
+
console.log("[UPLOAD] Detected zip upload", {
|
|
377
|
+
filename: formData.filename,
|
|
378
|
+
contentType: formData.contentType,
|
|
379
|
+
bufferSize: formData.contentBuffer.length,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Handle zip upload
|
|
383
|
+
try {
|
|
384
|
+
const { seedObject, artifacts } = await extractSeedZip(
|
|
385
|
+
formData.contentBuffer
|
|
386
|
+
);
|
|
387
|
+
console.log("[UPLOAD] Zip extraction completed", {
|
|
388
|
+
artifactCount: artifacts.length,
|
|
389
|
+
artifactNames: artifacts.map((a) => a.filename),
|
|
390
|
+
seedKeys: Object.keys(seedObject),
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
seedObject,
|
|
394
|
+
uploadArtifacts: artifacts,
|
|
395
|
+
};
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.log("[UPLOAD] Zip extraction failed", {
|
|
398
|
+
error: error.message,
|
|
399
|
+
filename: formData.filename,
|
|
400
|
+
});
|
|
401
|
+
// Re-throw zip-specific errors with clear messages
|
|
402
|
+
throw new Error(error.message);
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// Handle regular JSON file upload
|
|
406
|
+
try {
|
|
407
|
+
const seedObject = JSON.parse(formData.contentBuffer.toString("utf8"));
|
|
408
|
+
const filename = formData.filename || "seed.json";
|
|
409
|
+
return {
|
|
410
|
+
seedObject,
|
|
411
|
+
uploadArtifacts: [{ filename, content: formData.contentBuffer }],
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new Error("Invalid JSON");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
301
419
|
function broadcastStateUpdate(currentState) {
|
|
302
420
|
try {
|
|
303
421
|
const recentChanges = (currentState && currentState.recentChanges) || [];
|
|
@@ -365,7 +483,7 @@ function startHeartbeat() {
|
|
|
365
483
|
/**
|
|
366
484
|
* Parse multipart form data
|
|
367
485
|
* @param {http.IncomingMessage} req - HTTP request
|
|
368
|
-
* @returns {Promise<Object>} Parsed form data with file content
|
|
486
|
+
* @returns {Promise<Object>} Parsed form data with file content as Buffer
|
|
369
487
|
*/
|
|
370
488
|
function parseMultipartFormData(req) {
|
|
371
489
|
return new Promise((resolve, reject) => {
|
|
@@ -394,47 +512,54 @@ function parseMultipartFormData(req) {
|
|
|
394
512
|
req.on("end", () => {
|
|
395
513
|
try {
|
|
396
514
|
const buffer = Buffer.concat(chunks);
|
|
397
|
-
const data = buffer.toString("utf8");
|
|
398
|
-
console.log("Raw multipart data length:", data.length);
|
|
399
|
-
console.log("Boundary:", JSON.stringify(boundary));
|
|
400
515
|
|
|
401
|
-
//
|
|
516
|
+
// Find file part in the buffer using string operations for headers
|
|
517
|
+
const data = buffer.toString(
|
|
518
|
+
"utf8",
|
|
519
|
+
0,
|
|
520
|
+
Math.min(buffer.length, 1024 * 1024)
|
|
521
|
+
); // First MB for header search
|
|
402
522
|
const parts = data.split(boundary);
|
|
403
|
-
console.log("Number of parts:", parts.length);
|
|
404
523
|
|
|
405
524
|
for (let i = 0; i < parts.length; i++) {
|
|
406
525
|
const part = parts[i];
|
|
407
|
-
console.log(`Part ${i} length:`, part.length);
|
|
408
|
-
console.log(
|
|
409
|
-
`Part ${i} starts with:`,
|
|
410
|
-
JSON.stringify(part.substring(0, 50))
|
|
411
|
-
);
|
|
412
526
|
|
|
413
527
|
if (part.includes('name="file"') && part.includes("filename")) {
|
|
414
|
-
console.log("Found file part at index", i);
|
|
415
528
|
// Extract filename
|
|
416
529
|
const filenameMatch = part.match(/filename="([^"]+)"/);
|
|
417
|
-
console.log("Filename match:", filenameMatch);
|
|
418
530
|
if (!filenameMatch) continue;
|
|
419
531
|
|
|
420
532
|
// Extract content type
|
|
421
533
|
const contentTypeMatch = part.match(/Content-Type:\s*([^\r\n]+)/);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
534
|
+
|
|
535
|
+
// Find this specific part's start in the data string
|
|
536
|
+
const partIndexInData = data.indexOf(part);
|
|
537
|
+
const headerEndInPart = part.indexOf("\r\n\r\n");
|
|
538
|
+
if (headerEndInPart === -1) {
|
|
539
|
+
reject(
|
|
540
|
+
new Error("Could not find end of headers in multipart part")
|
|
541
|
+
);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Calculate the actual byte positions in the buffer for this part
|
|
546
|
+
const headerEndInData = partIndexInData + headerEndInPart + 4;
|
|
547
|
+
|
|
548
|
+
// Use binary buffer to find the next boundary
|
|
549
|
+
const boundaryBuf = Buffer.from(boundary, "ascii");
|
|
550
|
+
const nextBoundaryIndex = buffer.indexOf(
|
|
551
|
+
boundaryBuf,
|
|
552
|
+
headerEndInData
|
|
432
553
|
);
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
554
|
+
const contentEndInData =
|
|
555
|
+
nextBoundaryIndex !== -1
|
|
556
|
+
? nextBoundaryIndex - 2 // Subtract 2 for \r\n before boundary
|
|
557
|
+
: buffer.length;
|
|
558
|
+
|
|
559
|
+
// Extract the file content as Buffer
|
|
560
|
+
const contentBuffer = buffer.slice(
|
|
561
|
+
headerEndInData,
|
|
562
|
+
contentEndInData
|
|
438
563
|
);
|
|
439
564
|
|
|
440
565
|
resolve({
|
|
@@ -442,13 +567,12 @@ function parseMultipartFormData(req) {
|
|
|
442
567
|
contentType: contentTypeMatch
|
|
443
568
|
? contentTypeMatch[1]
|
|
444
569
|
: "application/octet-stream",
|
|
445
|
-
|
|
570
|
+
contentBuffer: contentBuffer,
|
|
446
571
|
});
|
|
447
572
|
return;
|
|
448
573
|
}
|
|
449
574
|
}
|
|
450
575
|
|
|
451
|
-
console.log("No file field found in form data");
|
|
452
576
|
reject(new Error("No file field found in form data"));
|
|
453
577
|
} catch (error) {
|
|
454
578
|
console.error("Error parsing multipart:", error);
|
|
@@ -466,57 +590,64 @@ function parseMultipartFormData(req) {
|
|
|
466
590
|
* @param {http.ServerResponse} res - HTTP response
|
|
467
591
|
*/
|
|
468
592
|
async function handleSeedUpload(req, res) {
|
|
593
|
+
// Add logging at the very start of the upload handler
|
|
594
|
+
console.log("[UPLOAD] Incoming seed upload", {
|
|
595
|
+
method: req.method,
|
|
596
|
+
url: req.url,
|
|
597
|
+
contentType: req.headers["content-type"],
|
|
598
|
+
userAgent: req.headers["user-agent"],
|
|
599
|
+
});
|
|
600
|
+
|
|
469
601
|
try {
|
|
470
602
|
const ct = req.headers["content-type"] || "";
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
try {
|
|
492
|
-
seedObject = JSON.parse(formData.content);
|
|
493
|
-
} catch {
|
|
494
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
495
|
-
res.end(JSON.stringify({ success: false, message: "Invalid JSON" }));
|
|
496
|
-
return;
|
|
603
|
+
|
|
604
|
+
// Use the new normalization function to handle all upload formats
|
|
605
|
+
let normalizedUpload;
|
|
606
|
+
try {
|
|
607
|
+
normalizedUpload = await normalizeSeedUpload({
|
|
608
|
+
req,
|
|
609
|
+
contentTypeHeader: ct,
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
console.log("[UPLOAD] Normalization failed", {
|
|
613
|
+
error: error.message,
|
|
614
|
+
contentType: ct,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Handle specific zip-related errors with appropriate messages
|
|
618
|
+
let errorMessage = error.message;
|
|
619
|
+
if (error.message === "Invalid JSON") {
|
|
620
|
+
errorMessage = "Invalid JSON";
|
|
621
|
+
} else if (error.message === "seed.json not found in zip") {
|
|
622
|
+
errorMessage = "seed.json not found in zip";
|
|
497
623
|
}
|
|
624
|
+
|
|
625
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
626
|
+
res.end(JSON.stringify({ success: false, message: errorMessage }));
|
|
627
|
+
return;
|
|
498
628
|
}
|
|
499
629
|
|
|
630
|
+
const { seedObject, uploadArtifacts } = normalizedUpload;
|
|
631
|
+
|
|
500
632
|
// Use current PO_ROOT or fallback to DATA_DIR
|
|
501
633
|
const currentDataDir = process.env.PO_ROOT || DATA_DIR;
|
|
502
634
|
|
|
503
635
|
// For test environment, use simplified validation without starting orchestrator
|
|
504
|
-
console.log("NODE_ENV:", process.env.NODE_ENV);
|
|
505
636
|
if (process.env.NODE_ENV === "test") {
|
|
506
|
-
console.log("Using test mode for seed upload");
|
|
507
637
|
// Simplified validation for tests - just write to pending directory
|
|
508
|
-
const result = await handleSeedUploadDirect(
|
|
509
|
-
|
|
638
|
+
const result = await handleSeedUploadDirect(
|
|
639
|
+
seedObject,
|
|
640
|
+
currentDataDir,
|
|
641
|
+
uploadArtifacts
|
|
642
|
+
);
|
|
510
643
|
|
|
511
644
|
// Return appropriate status code based on success
|
|
512
645
|
if (result.success) {
|
|
513
|
-
console.log("Sending 200 response");
|
|
514
646
|
res.writeHead(200, {
|
|
515
647
|
"Content-Type": "application/json",
|
|
516
648
|
Connection: "close",
|
|
517
649
|
});
|
|
518
650
|
res.end(JSON.stringify(result));
|
|
519
|
-
console.log("Response sent successfully");
|
|
520
651
|
|
|
521
652
|
// Broadcast SSE event for successful upload
|
|
522
653
|
sseRegistry.broadcast({
|
|
@@ -524,17 +655,13 @@ async function handleSeedUpload(req, res) {
|
|
|
524
655
|
data: { name: result.jobName },
|
|
525
656
|
});
|
|
526
657
|
} else {
|
|
527
|
-
console.log("Sending 400 response");
|
|
528
658
|
res.writeHead(400, {
|
|
529
659
|
"Content-Type": "application/json",
|
|
530
660
|
Connection: "close",
|
|
531
661
|
});
|
|
532
662
|
res.end(JSON.stringify(result));
|
|
533
|
-
console.log("Response sent successfully");
|
|
534
663
|
}
|
|
535
664
|
return;
|
|
536
|
-
} else {
|
|
537
|
-
console.log("Using production mode for seed upload");
|
|
538
665
|
}
|
|
539
666
|
|
|
540
667
|
// Submit job with validation (for production)
|
|
@@ -546,6 +673,7 @@ async function handleSeedUpload(req, res) {
|
|
|
546
673
|
const result = await submitJobWithValidation({
|
|
547
674
|
dataDir: currentDataDir,
|
|
548
675
|
seedObject,
|
|
676
|
+
uploadArtifacts,
|
|
549
677
|
});
|
|
550
678
|
|
|
551
679
|
// Send appropriate response
|
|
@@ -1082,7 +1210,6 @@ function serveStatic(res, filePath) {
|
|
|
1082
1210
|
* Create and start the HTTP server
|
|
1083
1211
|
*/
|
|
1084
1212
|
function createServer() {
|
|
1085
|
-
console.log("Creating HTTP server...");
|
|
1086
1213
|
const server = http.createServer(async (req, res) => {
|
|
1087
1214
|
// Use WHATWG URL API instead of deprecated url.parse
|
|
1088
1215
|
const { pathname, searchParams } = new URL(
|
|
@@ -1090,11 +1217,6 @@ function createServer() {
|
|
|
1090
1217
|
`http://${req.headers.host}`
|
|
1091
1218
|
);
|
|
1092
1219
|
|
|
1093
|
-
// DEBUG: Log all API requests
|
|
1094
|
-
if (pathname.startsWith("/api/")) {
|
|
1095
|
-
console.log(`DEBUG: API Request: ${req.method} ${pathname}`);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
1220
|
// CORS headers for API endpoints
|
|
1099
1221
|
if (pathname.startsWith("/api/")) {
|
|
1100
1222
|
// Important for tests: avoid idle keep-alive sockets on short API calls
|
|
@@ -1593,10 +1715,6 @@ function createServer() {
|
|
|
1593
1715
|
mode: "clean-slate",
|
|
1594
1716
|
spawned: true,
|
|
1595
1717
|
});
|
|
1596
|
-
|
|
1597
|
-
console.log(
|
|
1598
|
-
`Job ${jobId} restarted successfully, detached runner PID: ${child.pid}`
|
|
1599
|
-
);
|
|
1600
1718
|
} finally {
|
|
1601
1719
|
// Always end restart guard
|
|
1602
1720
|
endRestart(jobId);
|
|
@@ -1860,13 +1978,6 @@ function start(customPort) {
|
|
|
1860
1978
|
*/
|
|
1861
1979
|
async function startServer({ dataDir, port: customPort }) {
|
|
1862
1980
|
try {
|
|
1863
|
-
console.log(
|
|
1864
|
-
"DEBUG: startServer called with dataDir:",
|
|
1865
|
-
dataDir,
|
|
1866
|
-
"customPort:",
|
|
1867
|
-
customPort
|
|
1868
|
-
);
|
|
1869
|
-
|
|
1870
1981
|
// Initialize config-bridge paths early to ensure consistent path resolution
|
|
1871
1982
|
// This prevents path caching issues when dataDir changes between tests
|
|
1872
1983
|
const { initPATHS } = await import("./config-bridge.node.js");
|
|
@@ -1897,8 +2008,6 @@ async function startServer({ dataDir, port: customPort }) {
|
|
|
1897
2008
|
? parseInt(process.env.PORT)
|
|
1898
2009
|
: 0;
|
|
1899
2010
|
|
|
1900
|
-
console.log("DEBUG: About to create server...");
|
|
1901
|
-
|
|
1902
2011
|
// In development, start Vite in middlewareMode so the Node server can serve
|
|
1903
2012
|
// the client with HMR in a single process. We dynamically import Vite here
|
|
1904
2013
|
// to avoid including it in production bundles.
|
|
@@ -1916,22 +2025,15 @@ async function startServer({ dataDir, port: customPort }) {
|
|
|
1916
2025
|
server: { middlewareMode: true },
|
|
1917
2026
|
appType: "custom",
|
|
1918
2027
|
});
|
|
1919
|
-
console.log("DEBUG: Vite dev server started (middleware mode)");
|
|
1920
2028
|
} catch (err) {
|
|
1921
2029
|
console.error("Failed to start Vite dev server:", err);
|
|
1922
2030
|
viteServer = null;
|
|
1923
2031
|
}
|
|
1924
|
-
} else if (process.env.NODE_ENV === "test") {
|
|
1925
|
-
console.log("DEBUG: Vite disabled in test mode (API-only mode)");
|
|
1926
|
-
} else if (process.env.DISABLE_VITE === "1") {
|
|
1927
|
-
console.log("DEBUG: Vite disabled via DISABLE_VITE=1 (API-only mode)");
|
|
1928
2032
|
}
|
|
1929
2033
|
|
|
1930
2034
|
const server = createServer();
|
|
1931
|
-
console.log("DEBUG: Server created successfully");
|
|
1932
2035
|
|
|
1933
2036
|
// Robust promise with proper error handling and race condition prevention
|
|
1934
|
-
console.log(`Attempting to start server on port ${port}...`);
|
|
1935
2037
|
await new Promise((resolve, reject) => {
|
|
1936
2038
|
let settled = false;
|
|
1937
2039
|
|
|
@@ -1955,7 +2057,6 @@ async function startServer({ dataDir, port: customPort }) {
|
|
|
1955
2057
|
if (!settled) {
|
|
1956
2058
|
settled = true;
|
|
1957
2059
|
server.removeListener("error", errorHandler);
|
|
1958
|
-
console.log(`Server successfully started on port ${port}`);
|
|
1959
2060
|
resolve();
|
|
1960
2061
|
}
|
|
1961
2062
|
};
|
|
@@ -1981,18 +2082,10 @@ async function startServer({ dataDir, port: customPort }) {
|
|
|
1981
2082
|
const address = server.address();
|
|
1982
2083
|
const baseUrl = `http://localhost:${address.port}`;
|
|
1983
2084
|
|
|
1984
|
-
console.log(`Server running at ${baseUrl}`);
|
|
1985
|
-
if (dataDir) {
|
|
1986
|
-
console.log(`Data directory: ${dataDir}`);
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
2085
|
// Only initialize watcher and heartbeat in non-test environments
|
|
1990
2086
|
if (process.env.NODE_ENV !== "test") {
|
|
1991
|
-
console.log(`Watching paths: ${WATCHED_PATHS.join(", ")}`);
|
|
1992
2087
|
initializeWatcher();
|
|
1993
2088
|
startHeartbeat();
|
|
1994
|
-
} else {
|
|
1995
|
-
console.log("Server started in test mode - skipping watcher/heartbeat");
|
|
1996
2089
|
}
|
|
1997
2090
|
|
|
1998
2091
|
return {
|
|
@@ -2016,7 +2109,6 @@ async function startServer({ dataDir, port: customPort }) {
|
|
|
2016
2109
|
try {
|
|
2017
2110
|
await viteServer.close();
|
|
2018
2111
|
viteServer = null;
|
|
2019
|
-
console.log("DEBUG: Vite dev server closed");
|
|
2020
2112
|
} catch (err) {
|
|
2021
2113
|
console.error("Error closing Vite dev server:", err);
|
|
2022
2114
|
}
|
|
@@ -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
|
+
}
|