flowstride 1.0.18 → 1.0.20-rc.2
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/NATIVE_CI.md +155 -0
- package/README.md +40 -33
- package/RELEASE_NOTES.md +40 -0
- package/dist/adapters/web/playwright.d.ts +4 -0
- package/dist/adapters/web/playwright.js +85 -38
- package/dist/ci/config.d.ts +3 -0
- package/dist/ci/config.js +76 -0
- package/dist/ci/deadline.d.ts +2 -0
- package/dist/ci/deadline.js +16 -0
- package/dist/ci/init.d.ts +1 -0
- package/dist/ci/init.js +21 -0
- package/dist/ci/runtime.d.ts +19 -0
- package/dist/ci/runtime.js +190 -0
- package/dist/cli/commands/open.d.ts +1 -0
- package/dist/cli/commands/open.js +12 -0
- package/dist/cli/commands/run.js +15 -34
- package/dist/cli/index.js +51 -5
- package/dist/cli/manual_server/workspace_server.d.ts +1 -0
- package/dist/cli/manual_server/workspace_server.js +751 -0
- package/dist/game/bridge.d.ts +13 -0
- package/dist/game/bridge.js +15 -0
- package/dist/game/sdk.d.ts +27 -0
- package/dist/game/sdk.js +255 -0
- package/dist/game/three-bridge.d.ts +18 -0
- package/dist/game/three-bridge.js +59 -0
- package/dist/parser/lexer.js +4 -0
- package/dist/parser/parser.js +19 -3
- package/dist/runner/flow_orchestrator.d.ts +5 -1
- package/dist/runner/flow_orchestrator.js +31 -7
- package/dist/runner/flow_worker.js +57 -13
- package/dist/runner/plugin_manager.d.ts +1 -0
- package/dist/runner/plugin_manager.js +33 -1
- package/dist/types/index.d.ts +36 -0
- package/examples/native-game/flows/canvas.flow +13 -0
- package/examples/native-game/flowstride.config.json +12 -0
- package/examples/native-game/server.mjs +12 -0
- package/package.json +14 -5
- package/src/ui/dist/assets/index-DIC22ZK_.js +638 -0
- package/src/ui/dist/assets/workspace_engine_bg-BVlpAZ1H.wasm +0 -0
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/index-BCdexgt5.js +0 -270
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.startManualWorkspaceServer = startManualWorkspaceServer;
|
|
40
|
+
const http = __importStar(require("http"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const ws_1 = require("ws");
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
async function startManualWorkspaceServer() {
|
|
46
|
+
const cliInstallPath = path.resolve(__dirname, "../../..");
|
|
47
|
+
const uiDistPath = path.join(cliInstallPath, "src", "ui", "dist");
|
|
48
|
+
if (!fs.existsSync(uiDistPath)) {
|
|
49
|
+
console.error(chalk_1.default.red(`\n❌ Error: UI build bundle not found at ${uiDistPath}. Please build the UI first.`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const manualFlowsDir = path.join(process.cwd(), "manual_flows");
|
|
53
|
+
const templatesDir = path.join(manualFlowsDir, "templates");
|
|
54
|
+
const runsDir = path.join(manualFlowsDir, "runs");
|
|
55
|
+
const mediaDir = path.join(manualFlowsDir, "media");
|
|
56
|
+
const defectMediaDir = path.join(manualFlowsDir, "defect_media");
|
|
57
|
+
const workspaceStateFile = path.join(manualFlowsDir, ".workspace_state.json");
|
|
58
|
+
if (!fs.existsSync(templatesDir))
|
|
59
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
60
|
+
if (!fs.existsSync(runsDir))
|
|
61
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
62
|
+
if (!fs.existsSync(mediaDir))
|
|
63
|
+
fs.mkdirSync(mediaDir, { recursive: true });
|
|
64
|
+
if (!fs.existsSync(defectMediaDir))
|
|
65
|
+
fs.mkdirSync(defectMediaDir, { recursive: true });
|
|
66
|
+
const mimeTypes = {
|
|
67
|
+
".png": "image/png",
|
|
68
|
+
".jpg": "image/jpeg",
|
|
69
|
+
".jpeg": "image/jpeg",
|
|
70
|
+
".gif": "image/gif",
|
|
71
|
+
".mp4": "video/mp4",
|
|
72
|
+
".webm": "video/webm",
|
|
73
|
+
};
|
|
74
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
75
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
76
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
77
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-File-Extension");
|
|
78
|
+
if (req.method === "OPTIONS") {
|
|
79
|
+
res.writeHead(204);
|
|
80
|
+
return res.end();
|
|
81
|
+
}
|
|
82
|
+
const urlObj = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
83
|
+
const pathname = urlObj.pathname;
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// VAULT 1: DESIGN REFERENCES (/media) -> STRICTLY LOCAL ONLY
|
|
86
|
+
// ============================================================================
|
|
87
|
+
if (req.method === "POST" && pathname === "/api/upload") {
|
|
88
|
+
const ext = req.headers["x-file-extension"] || "";
|
|
89
|
+
const allowedExts = [".png", ".jpg", ".jpeg", ".gif", ".mp4", ".webm"];
|
|
90
|
+
if (!ext || !allowedExts.includes(ext.toLowerCase())) {
|
|
91
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
92
|
+
return res.end(JSON.stringify({ error: "Invalid or missing file extension." }));
|
|
93
|
+
}
|
|
94
|
+
const safeFilename = `media_${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext.toLowerCase()}`;
|
|
95
|
+
const filePath = path.join(mediaDir, safeFilename);
|
|
96
|
+
const MAX_SIZE = 104857600;
|
|
97
|
+
let uploadedSize = 0;
|
|
98
|
+
let isAborted = false;
|
|
99
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
100
|
+
req.on("data", (chunk) => {
|
|
101
|
+
if (isAborted)
|
|
102
|
+
return;
|
|
103
|
+
uploadedSize += chunk.length;
|
|
104
|
+
if (uploadedSize > MAX_SIZE) {
|
|
105
|
+
isAborted = true;
|
|
106
|
+
writeStream.destroy();
|
|
107
|
+
if (fs.existsSync(filePath))
|
|
108
|
+
fs.unlinkSync(filePath);
|
|
109
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
110
|
+
res.end(JSON.stringify({ error: "Payload Too Large. Max size is 100MB." }));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
writeStream.write(chunk);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
req.on("end", () => {
|
|
117
|
+
if (isAborted)
|
|
118
|
+
return;
|
|
119
|
+
writeStream.end();
|
|
120
|
+
console.log(chalk_1.default.blue(`\n[Reference Uploaded]: Securely saved ${safeFilename} (${(uploadedSize / 1024 / 1024).toFixed(2)} MB)`));
|
|
121
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
122
|
+
res.end(JSON.stringify({ url: `/media/${safeFilename}` }));
|
|
123
|
+
});
|
|
124
|
+
req.on("error", (err) => {
|
|
125
|
+
if (!isAborted) {
|
|
126
|
+
writeStream.destroy();
|
|
127
|
+
if (fs.existsSync(filePath))
|
|
128
|
+
fs.unlinkSync(filePath);
|
|
129
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
130
|
+
res.end(JSON.stringify({ error: "Upload stream failed" }));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (req.method === "DELETE" && pathname === "/api/media") {
|
|
136
|
+
try {
|
|
137
|
+
const targetPath = urlObj.searchParams.get("filepath");
|
|
138
|
+
if (!targetPath) {
|
|
139
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
140
|
+
return res.end(JSON.stringify({ error: "filepath parameter is required" }));
|
|
141
|
+
}
|
|
142
|
+
const rawFilename = targetPath.replace("/media/", "");
|
|
143
|
+
const safeFilename = path.basename(rawFilename);
|
|
144
|
+
const filePath = path.join(mediaDir, safeFilename);
|
|
145
|
+
if (!fs.existsSync(filePath)) {
|
|
146
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
147
|
+
return res.end(JSON.stringify({ error: "Media file not found in vault" }));
|
|
148
|
+
}
|
|
149
|
+
fs.unlinkSync(filePath);
|
|
150
|
+
console.log(chalk_1.default.yellow(`\n[Reference Deleted]: Asset securely removed from manual_flows/media/${safeFilename}`));
|
|
151
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
152
|
+
res.end(JSON.stringify({
|
|
153
|
+
success: true,
|
|
154
|
+
message: "Media physically deleted",
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to delete media - ${e.message}`));
|
|
159
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
160
|
+
res.end(JSON.stringify({
|
|
161
|
+
error: "Internal server error while deleting media.",
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (req.method === "GET" && pathname.startsWith("/media/")) {
|
|
167
|
+
const filename = pathname.replace("/media/", "");
|
|
168
|
+
const safeFilename = path.basename(filename);
|
|
169
|
+
const filePath = path.join(mediaDir, safeFilename);
|
|
170
|
+
if (!fs.existsSync(filePath)) {
|
|
171
|
+
res.writeHead(404);
|
|
172
|
+
return res.end("Media not found");
|
|
173
|
+
}
|
|
174
|
+
const stat = fs.statSync(filePath);
|
|
175
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
176
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
177
|
+
const range = req.headers.range;
|
|
178
|
+
if (range) {
|
|
179
|
+
const parts = range.replace(/bytes=/, "").split("-");
|
|
180
|
+
const start = parseInt(parts[0], 10);
|
|
181
|
+
const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
|
|
182
|
+
if (start >= stat.size || end >= stat.size || start > end) {
|
|
183
|
+
res.writeHead(416, { "Content-Range": `bytes */${stat.size}` });
|
|
184
|
+
return res.end();
|
|
185
|
+
}
|
|
186
|
+
const chunksize = end - start + 1;
|
|
187
|
+
const file = fs.createReadStream(filePath, { start, end });
|
|
188
|
+
res.writeHead(206, {
|
|
189
|
+
"Content-Range": `bytes ${start}-${end}/${stat.size}`,
|
|
190
|
+
"Accept-Ranges": "bytes",
|
|
191
|
+
"Content-Length": chunksize,
|
|
192
|
+
"Content-Type": contentType,
|
|
193
|
+
});
|
|
194
|
+
file.pipe(res);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
res.writeHead(200, {
|
|
198
|
+
"Content-Length": stat.size,
|
|
199
|
+
"Content-Type": contentType,
|
|
200
|
+
});
|
|
201
|
+
fs.createReadStream(filePath).pipe(res);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// VAULT 2: DEFECT EVIDENCE (/defect_media) -> STRICTLY LOCAL ONLY
|
|
207
|
+
// ============================================================================
|
|
208
|
+
if (req.method === "POST" && pathname === "/api/upload-defect") {
|
|
209
|
+
const ext = req.headers["x-file-extension"] || "";
|
|
210
|
+
const allowedExts = [".png", ".jpg", ".jpeg", ".gif", ".mp4", ".webm"];
|
|
211
|
+
if (!ext || !allowedExts.includes(ext.toLowerCase())) {
|
|
212
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
213
|
+
return res.end(JSON.stringify({ error: "Invalid or missing file extension." }));
|
|
214
|
+
}
|
|
215
|
+
const safeFilename = `defect_${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext.toLowerCase()}`;
|
|
216
|
+
const filePath = path.join(defectMediaDir, safeFilename);
|
|
217
|
+
const MAX_SIZE = 41943040; // Capped at 40MB
|
|
218
|
+
let uploadedSize = 0;
|
|
219
|
+
let isAborted = false;
|
|
220
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
221
|
+
req.on("data", (chunk) => {
|
|
222
|
+
if (isAborted)
|
|
223
|
+
return;
|
|
224
|
+
uploadedSize += chunk.length;
|
|
225
|
+
if (uploadedSize > MAX_SIZE) {
|
|
226
|
+
isAborted = true;
|
|
227
|
+
writeStream.destroy();
|
|
228
|
+
if (fs.existsSync(filePath))
|
|
229
|
+
fs.unlinkSync(filePath);
|
|
230
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
231
|
+
res.end(JSON.stringify({ error: "Payload Too Large. Max size is 40MB." }));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
writeStream.write(chunk);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
req.on("end", async () => {
|
|
238
|
+
if (isAborted)
|
|
239
|
+
return;
|
|
240
|
+
writeStream.end();
|
|
241
|
+
const finalUrl = `/defect_media/${safeFilename}`;
|
|
242
|
+
console.log(chalk_1.default.green(`\n[Vault Success]: Defect Media secured locally at -> ${finalUrl}`));
|
|
243
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
244
|
+
res.end(JSON.stringify({ url: finalUrl }));
|
|
245
|
+
});
|
|
246
|
+
req.on("error", (err) => {
|
|
247
|
+
if (!isAborted) {
|
|
248
|
+
writeStream.destroy();
|
|
249
|
+
if (fs.existsSync(filePath))
|
|
250
|
+
fs.unlinkSync(filePath);
|
|
251
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
252
|
+
res.end(JSON.stringify({ error: "Upload stream failed" }));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (req.method === "DELETE" && pathname === "/api/defect-media") {
|
|
258
|
+
try {
|
|
259
|
+
const targetPath = urlObj.searchParams.get("filepath");
|
|
260
|
+
if (!targetPath) {
|
|
261
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
262
|
+
return res.end(JSON.stringify({ error: "filepath parameter is required" }));
|
|
263
|
+
}
|
|
264
|
+
const rawFilename = targetPath.replace("/defect_media/", "");
|
|
265
|
+
const safeFilename = path.basename(rawFilename);
|
|
266
|
+
const filePath = path.join(defectMediaDir, safeFilename);
|
|
267
|
+
if (fs.existsSync(filePath)) {
|
|
268
|
+
fs.unlinkSync(filePath);
|
|
269
|
+
console.log(chalk_1.default.yellow(`\n[Local Cache Cleared]: Asset securely removed from manual_flows/defect_media/${safeFilename}`));
|
|
270
|
+
}
|
|
271
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
272
|
+
res.end(JSON.stringify({
|
|
273
|
+
success: true,
|
|
274
|
+
message: "Defect media processed for deletion",
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to delete defect media - ${e.message}`));
|
|
279
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
280
|
+
res.end(JSON.stringify({
|
|
281
|
+
error: "Internal server error while deleting defect media.",
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (req.method === "GET" && pathname.startsWith("/defect_media/")) {
|
|
287
|
+
const filename = pathname.replace("/defect_media/", "");
|
|
288
|
+
const safeFilename = path.basename(filename);
|
|
289
|
+
const filePath = path.join(defectMediaDir, safeFilename);
|
|
290
|
+
if (!fs.existsSync(filePath)) {
|
|
291
|
+
res.writeHead(404);
|
|
292
|
+
return res.end("Defect media not found");
|
|
293
|
+
}
|
|
294
|
+
const stat = fs.statSync(filePath);
|
|
295
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
296
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
297
|
+
const range = req.headers.range;
|
|
298
|
+
if (range) {
|
|
299
|
+
const parts = range.replace(/bytes=/, "").split("-");
|
|
300
|
+
const start = parseInt(parts[0], 10);
|
|
301
|
+
const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
|
|
302
|
+
if (start >= stat.size || end >= stat.size || start > end) {
|
|
303
|
+
res.writeHead(416, { "Content-Range": `bytes */${stat.size}` });
|
|
304
|
+
return res.end();
|
|
305
|
+
}
|
|
306
|
+
const chunksize = end - start + 1;
|
|
307
|
+
const file = fs.createReadStream(filePath, { start, end });
|
|
308
|
+
res.writeHead(206, {
|
|
309
|
+
"Content-Range": `bytes ${start}-${end}/${stat.size}`,
|
|
310
|
+
"Accept-Ranges": "bytes",
|
|
311
|
+
"Content-Length": chunksize,
|
|
312
|
+
"Content-Type": contentType,
|
|
313
|
+
});
|
|
314
|
+
file.pipe(res);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
res.writeHead(200, {
|
|
318
|
+
"Content-Length": stat.size,
|
|
319
|
+
"Content-Type": contentType,
|
|
320
|
+
});
|
|
321
|
+
fs.createReadStream(filePath).pipe(res);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// CORE SYSTEM ROUTES
|
|
327
|
+
// ============================================================================
|
|
328
|
+
if (req.method === "POST" && pathname === "/api/save-design") {
|
|
329
|
+
let body = "";
|
|
330
|
+
req.on("data", (chunk) => {
|
|
331
|
+
body += chunk.toString();
|
|
332
|
+
});
|
|
333
|
+
req.on("end", () => {
|
|
334
|
+
try {
|
|
335
|
+
const payload = JSON.parse(body);
|
|
336
|
+
const rawFilename = payload.filename || `New Flow.json`;
|
|
337
|
+
const canvasData = payload.canvasData;
|
|
338
|
+
if (!canvasData) {
|
|
339
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
340
|
+
return res.end(JSON.stringify({ error: "Missing canvasData in payload" }));
|
|
341
|
+
}
|
|
342
|
+
let safeFilename = path.basename(rawFilename);
|
|
343
|
+
if (!safeFilename.endsWith(".json")) {
|
|
344
|
+
safeFilename += ".json";
|
|
345
|
+
}
|
|
346
|
+
const filePath = path.join(templatesDir, safeFilename);
|
|
347
|
+
fs.writeFileSync(filePath, JSON.stringify(canvasData, null, 2), "utf-8");
|
|
348
|
+
console.log(chalk_1.default.green(`\n[Saved]: Template securely vaulted to manual_flows/templates/${safeFilename}`));
|
|
349
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
350
|
+
res.end(JSON.stringify({
|
|
351
|
+
success: true,
|
|
352
|
+
message: "Template saved securely",
|
|
353
|
+
filename: safeFilename,
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to save template - ${e.message}`));
|
|
358
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
359
|
+
res.end(JSON.stringify({
|
|
360
|
+
error: "Internal server error while saving template.",
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (req.method === "GET" && pathname === "/api/templates") {
|
|
367
|
+
try {
|
|
368
|
+
const files = fs
|
|
369
|
+
.readdirSync(templatesDir)
|
|
370
|
+
.filter((f) => f.endsWith(".json"));
|
|
371
|
+
const templates = files.map((file) => ({
|
|
372
|
+
id: file,
|
|
373
|
+
name: file.replace(".json", ""),
|
|
374
|
+
}));
|
|
375
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
376
|
+
res.end(JSON.stringify({ templates }));
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to read templates directory - ${e.message}`));
|
|
380
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
381
|
+
res.end(JSON.stringify({
|
|
382
|
+
error: "Internal server error while reading templates.",
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (req.method === "GET" && pathname === "/api/template") {
|
|
388
|
+
try {
|
|
389
|
+
const targetFilename = urlObj.searchParams.get("filename");
|
|
390
|
+
if (!targetFilename) {
|
|
391
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
392
|
+
return res.end(JSON.stringify({ error: "Filename parameter is required" }));
|
|
393
|
+
}
|
|
394
|
+
const safeFilename = path.basename(targetFilename);
|
|
395
|
+
if (!safeFilename.endsWith(".json")) {
|
|
396
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
397
|
+
return res.end(JSON.stringify({
|
|
398
|
+
error: "Forbidden: Only JSON configuration files can be loaded",
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
const filePath = path.join(templatesDir, safeFilename);
|
|
402
|
+
if (!fs.existsSync(filePath)) {
|
|
403
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
404
|
+
return res.end(JSON.stringify({ error: "Template file not found" }));
|
|
405
|
+
}
|
|
406
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
407
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
408
|
+
res.end(fileContent);
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to load template - ${e.message}`));
|
|
412
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
413
|
+
res.end(JSON.stringify({
|
|
414
|
+
error: "Internal server error while loading template.",
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (req.method === "DELETE" && pathname === "/api/template") {
|
|
420
|
+
try {
|
|
421
|
+
const targetFilename = urlObj.searchParams.get("filename");
|
|
422
|
+
if (!targetFilename) {
|
|
423
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
424
|
+
return res.end(JSON.stringify({ error: "Filename parameter is required" }));
|
|
425
|
+
}
|
|
426
|
+
const safeFilename = path.basename(targetFilename);
|
|
427
|
+
if (!safeFilename.endsWith(".json")) {
|
|
428
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
429
|
+
return res.end(JSON.stringify({
|
|
430
|
+
error: "Forbidden: Only JSON configuration files can be deleted",
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
const filePath = path.join(templatesDir, safeFilename);
|
|
434
|
+
if (!fs.existsSync(filePath)) {
|
|
435
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
436
|
+
return res.end(JSON.stringify({ error: "Template file not found" }));
|
|
437
|
+
}
|
|
438
|
+
fs.unlinkSync(filePath);
|
|
439
|
+
console.log(chalk_1.default.yellow(`\n[Deleted]: Template securely removed from manual_flows/templates/${safeFilename}`));
|
|
440
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
441
|
+
res.end(JSON.stringify({
|
|
442
|
+
success: true,
|
|
443
|
+
message: "Template deleted successfully",
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to delete template - ${e.message}`));
|
|
448
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
449
|
+
res.end(JSON.stringify({
|
|
450
|
+
error: "Internal server error while deleting template.",
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (req.method === "POST" && pathname === "/api/save-run") {
|
|
456
|
+
let body = "";
|
|
457
|
+
req.on("data", (chunk) => {
|
|
458
|
+
body += chunk.toString();
|
|
459
|
+
});
|
|
460
|
+
req.on("end", async () => {
|
|
461
|
+
try {
|
|
462
|
+
const payload = JSON.parse(body);
|
|
463
|
+
const rawFilename = payload.filename || `Execution_Run_${Date.now()}.json`;
|
|
464
|
+
const canvasData = payload.canvasData;
|
|
465
|
+
if (!canvasData) {
|
|
466
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
467
|
+
return res.end(JSON.stringify({ error: "Missing canvasData in payload" }));
|
|
468
|
+
}
|
|
469
|
+
const now = new Date();
|
|
470
|
+
const dayFolder = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
471
|
+
const timeFolder = `${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}-${String(now.getSeconds()).padStart(2, "0")}`;
|
|
472
|
+
const runTargetDir = path.join(runsDir, dayFolder, timeFolder);
|
|
473
|
+
if (!fs.existsSync(runTargetDir)) {
|
|
474
|
+
fs.mkdirSync(runTargetDir, { recursive: true });
|
|
475
|
+
}
|
|
476
|
+
let safeFilename = path.basename(rawFilename);
|
|
477
|
+
if (!safeFilename.endsWith(".json")) {
|
|
478
|
+
safeFilename += ".json";
|
|
479
|
+
}
|
|
480
|
+
const filePath = path.join(runTargetDir, safeFilename);
|
|
481
|
+
fs.writeFileSync(filePath, JSON.stringify(canvasData, null, 2), "utf-8");
|
|
482
|
+
const relativePath = path
|
|
483
|
+
.join(dayFolder, timeFolder, safeFilename)
|
|
484
|
+
.split(path.sep)
|
|
485
|
+
.join("/");
|
|
486
|
+
console.log(chalk_1.default.green(`\n[Saved]: Run securely vaulted to manual_flows/runs/${relativePath}`));
|
|
487
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
488
|
+
res.end(JSON.stringify({
|
|
489
|
+
success: true,
|
|
490
|
+
message: "Run saved securely",
|
|
491
|
+
filename: relativePath,
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to save run - ${e.message}`));
|
|
496
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
497
|
+
res.end(JSON.stringify({
|
|
498
|
+
error: "Internal server error while saving run.",
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (req.method === "GET" && pathname === "/api/runs") {
|
|
505
|
+
try {
|
|
506
|
+
const getAllRuns = (dir, baseDir) => {
|
|
507
|
+
let results = [];
|
|
508
|
+
if (!fs.existsSync(dir))
|
|
509
|
+
return results;
|
|
510
|
+
const list = fs.readdirSync(dir);
|
|
511
|
+
for (const file of list) {
|
|
512
|
+
const fullPath = path.join(dir, file);
|
|
513
|
+
const stat = fs.statSync(fullPath);
|
|
514
|
+
if (stat && stat.isDirectory()) {
|
|
515
|
+
results = results.concat(getAllRuns(fullPath, baseDir));
|
|
516
|
+
}
|
|
517
|
+
else if (file.endsWith(".json")) {
|
|
518
|
+
const relativePath = path
|
|
519
|
+
.relative(baseDir, fullPath)
|
|
520
|
+
.split(path.sep)
|
|
521
|
+
.join("/");
|
|
522
|
+
let status = "UNKNOWN";
|
|
523
|
+
let passed = 0;
|
|
524
|
+
let failed = 0;
|
|
525
|
+
let total = 0;
|
|
526
|
+
try {
|
|
527
|
+
const fileData = fs.readFileSync(fullPath, "utf-8");
|
|
528
|
+
const parsed = JSON.parse(fileData);
|
|
529
|
+
if (parsed.executionReport &&
|
|
530
|
+
Array.isArray(parsed.executionReport)) {
|
|
531
|
+
total = parsed.executionReport.length;
|
|
532
|
+
passed = parsed.executionReport.filter((s) => {
|
|
533
|
+
const safeStatus = String(s.status || "").toLowerCase();
|
|
534
|
+
return ["pass", "passed", "success"].includes(safeStatus);
|
|
535
|
+
}).length;
|
|
536
|
+
failed = parsed.executionReport.filter((s) => {
|
|
537
|
+
const safeStatus = String(s.status || "").toLowerCase();
|
|
538
|
+
return safeStatus === "failed" || safeStatus === "fail";
|
|
539
|
+
}).length;
|
|
540
|
+
if (total === 0) {
|
|
541
|
+
status = "NO STEPS";
|
|
542
|
+
}
|
|
543
|
+
else if (failed > 0) {
|
|
544
|
+
status = "FAIL";
|
|
545
|
+
}
|
|
546
|
+
else if (passed === total) {
|
|
547
|
+
status = "PASS";
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
status = "INCOMPLETE";
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (parseError) {
|
|
555
|
+
status = "CORRUPT";
|
|
556
|
+
}
|
|
557
|
+
results.push({
|
|
558
|
+
id: relativePath,
|
|
559
|
+
name: file.replace(".json", ""),
|
|
560
|
+
metrics: { status, passed, failed, total },
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return results;
|
|
565
|
+
};
|
|
566
|
+
const runs = getAllRuns(runsDir, runsDir);
|
|
567
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
568
|
+
res.end(JSON.stringify({ runs }));
|
|
569
|
+
}
|
|
570
|
+
catch (e) {
|
|
571
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to read runs directory - ${e.message}`));
|
|
572
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
573
|
+
res.end(JSON.stringify({
|
|
574
|
+
error: "Internal server error while reading runs.",
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (req.method === "GET" && pathname === "/api/run") {
|
|
580
|
+
try {
|
|
581
|
+
const targetFilename = urlObj.searchParams.get("filename");
|
|
582
|
+
if (!targetFilename) {
|
|
583
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
584
|
+
return res.end(JSON.stringify({ error: "Filename parameter is required" }));
|
|
585
|
+
}
|
|
586
|
+
if (targetFilename.includes("..") ||
|
|
587
|
+
targetFilename.startsWith("/") ||
|
|
588
|
+
targetFilename.startsWith("\\")) {
|
|
589
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
590
|
+
return res.end(JSON.stringify({ error: "Forbidden: Invalid file path" }));
|
|
591
|
+
}
|
|
592
|
+
const filePath = path.join(runsDir, targetFilename);
|
|
593
|
+
const resolvedPath = path.resolve(filePath);
|
|
594
|
+
const resolvedRunsDir = path.resolve(runsDir);
|
|
595
|
+
if (!resolvedPath.startsWith(resolvedRunsDir) ||
|
|
596
|
+
!resolvedPath.endsWith(".json")) {
|
|
597
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
598
|
+
return res.end(JSON.stringify({
|
|
599
|
+
error: "Forbidden: Unauthorized file access detected",
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
if (!fs.existsSync(filePath)) {
|
|
603
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
604
|
+
return res.end(JSON.stringify({ error: "Run file not found" }));
|
|
605
|
+
}
|
|
606
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
607
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
608
|
+
res.end(fileContent);
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to load run - ${e.message}`));
|
|
612
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
613
|
+
res.end(JSON.stringify({ error: "Internal server error while loading run." }));
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (req.method === "GET" && pathname === "/api/state") {
|
|
618
|
+
try {
|
|
619
|
+
if (fs.existsSync(workspaceStateFile)) {
|
|
620
|
+
const stateContent = fs.readFileSync(workspaceStateFile, "utf-8");
|
|
621
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
622
|
+
res.end(stateContent);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
626
|
+
res.end(JSON.stringify({ activeFlowId: null }));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to load workspace state - ${e.message}`));
|
|
631
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
632
|
+
res.end(JSON.stringify({
|
|
633
|
+
error: "Internal server error while loading state.",
|
|
634
|
+
}));
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (req.method === "POST" && pathname === "/api/state") {
|
|
639
|
+
let body = "";
|
|
640
|
+
req.on("data", (chunk) => {
|
|
641
|
+
body += chunk.toString();
|
|
642
|
+
});
|
|
643
|
+
req.on("end", () => {
|
|
644
|
+
try {
|
|
645
|
+
const payload = JSON.parse(body);
|
|
646
|
+
const stateObj = {
|
|
647
|
+
activeFlowId: typeof payload.activeFlowId === "string"
|
|
648
|
+
? payload.activeFlowId
|
|
649
|
+
: null,
|
|
650
|
+
};
|
|
651
|
+
fs.writeFileSync(workspaceStateFile, JSON.stringify(stateObj, null, 2), "utf-8");
|
|
652
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
653
|
+
res.end(JSON.stringify({ success: true }));
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
console.error(chalk_1.default.red(`\n[API Error]: Failed to save workspace state - ${e.message}`));
|
|
657
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
658
|
+
res.end(JSON.stringify({
|
|
659
|
+
error: "Internal server error while saving state.",
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const reqPath = pathname === "/" ? "/index.html" : pathname;
|
|
666
|
+
const filePath = path.resolve(uiDistPath, path.join(".", reqPath));
|
|
667
|
+
if (!filePath.startsWith(uiDistPath + path.sep) &&
|
|
668
|
+
filePath !== uiDistPath) {
|
|
669
|
+
res.writeHead(403);
|
|
670
|
+
return res.end("Forbidden");
|
|
671
|
+
}
|
|
672
|
+
if (fs.existsSync(filePath)) {
|
|
673
|
+
const extname = path.extname(filePath);
|
|
674
|
+
let contentType = "text/html";
|
|
675
|
+
switch (extname) {
|
|
676
|
+
case ".js":
|
|
677
|
+
contentType = "application/javascript";
|
|
678
|
+
break;
|
|
679
|
+
case ".css":
|
|
680
|
+
contentType = "text/css";
|
|
681
|
+
break;
|
|
682
|
+
case ".json":
|
|
683
|
+
contentType = "application/json";
|
|
684
|
+
break;
|
|
685
|
+
case ".png":
|
|
686
|
+
contentType = "image/png";
|
|
687
|
+
break;
|
|
688
|
+
case ".svg":
|
|
689
|
+
contentType = "image/svg+xml";
|
|
690
|
+
break;
|
|
691
|
+
case ".ico":
|
|
692
|
+
contentType = "image/x-icon";
|
|
693
|
+
break;
|
|
694
|
+
case ".wasm":
|
|
695
|
+
contentType = "application/wasm";
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
699
|
+
fs.createReadStream(filePath).pipe(res);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
const reqExtname = path.extname(reqPath);
|
|
703
|
+
if (reqExtname && reqExtname !== ".html") {
|
|
704
|
+
res.writeHead(404);
|
|
705
|
+
return res.end("Static asset not found.");
|
|
706
|
+
}
|
|
707
|
+
const indexPath = path.join(uiDistPath, "index.html");
|
|
708
|
+
if (fs.existsSync(indexPath)) {
|
|
709
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
710
|
+
fs.createReadStream(indexPath).pipe(res);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
res.writeHead(404);
|
|
714
|
+
res.end("Flowstride UI bundle not found.");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
const wss = new ws_1.WebSocketServer({ server: httpServer });
|
|
719
|
+
wss.on("connection", (ws) => {
|
|
720
|
+
ws.send(JSON.stringify({ type: "CLI_CONTEXT", payload: { command: "open" } }));
|
|
721
|
+
ws.on("error", (err) => {
|
|
722
|
+
console.error(chalk_1.default.red(`[WebSocket Error]: ${err.message}`));
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
httpServer.on("error", (e) => {
|
|
726
|
+
console.error(chalk_1.default.red(`\n[Error]: Failed to start manual workspace server: ${e.message}`));
|
|
727
|
+
process.exit(1);
|
|
728
|
+
});
|
|
729
|
+
httpServer.listen(0, async () => {
|
|
730
|
+
try {
|
|
731
|
+
const address = httpServer.address();
|
|
732
|
+
const port = typeof address === "object" && address !== null ? address.port : null;
|
|
733
|
+
if (!port) {
|
|
734
|
+
console.error(chalk_1.default.red("\n[Error]: Could not bind to an available port."));
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
const localUrl = `http://localhost:${port}`;
|
|
738
|
+
console.log(chalk_1.default.yellow(`\n[Workspace]: Manual Design server running at ${localUrl}`));
|
|
739
|
+
const { default: open } = await import("open");
|
|
740
|
+
await open(localUrl);
|
|
741
|
+
console.log(chalk_1.default.green("[Workspace]: Manual UI launched successfully in browser. Press Ctrl+C to exit.\n"));
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
console.error(chalk_1.default.red(`\n[Error]: Failed to open browser: ${err.message}`));
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
process.on("SIGINT", () => {
|
|
748
|
+
httpServer.close();
|
|
749
|
+
process.exit(0);
|
|
750
|
+
});
|
|
751
|
+
}
|