sdn-flow 0.2.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/.claude/SKILLS.md +7 -0
- package/.claude/skills/sdn-plugin-abi-compliance/SKILL.md +56 -0
- package/.claude/todo/001-js-host-startup-and-deno.md +85 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/bin/sdn-flow-host.js +169 -0
- package/docs/.nojekyll +0 -0
- package/docs/ARCHITECTURE.md +200 -0
- package/docs/HOST_CAPABILITY_MODEL.md +317 -0
- package/docs/PLUGIN_ARCHITECTURE.md +145 -0
- package/docs/PLUGIN_COMPATIBILITY.md +61 -0
- package/docs/PLUGIN_COMPLIANCE_CHECKS.md +82 -0
- package/docs/PLUGIN_MANIFEST.md +94 -0
- package/docs/css/style.css +465 -0
- package/docs/index.html +218 -0
- package/docs/js/app.mjs +751 -0
- package/docs/js/editor-panel.mjs +203 -0
- package/docs/js/flow-canvas.mjs +515 -0
- package/docs/js/flow-model.mjs +391 -0
- package/docs/js/workers/emception.worker.js +146 -0
- package/docs/js/workers/pyodide.worker.js +134 -0
- package/native/flow_source_generator.cpp +1958 -0
- package/package.json +67 -0
- package/schemas/FlowRuntimeAbi.fbs +91 -0
- package/src/auth/canonicalize.js +5 -0
- package/src/auth/index.js +11 -0
- package/src/auth/permissions.js +8 -0
- package/src/compiler/CppFlowSourceGenerator.js +475 -0
- package/src/compiler/EmceptionCompilerAdapter.js +244 -0
- package/src/compiler/SignedArtifactCatalog.js +152 -0
- package/src/compiler/index.js +8 -0
- package/src/compiler/nativeFlowSourceGeneratorTool.js +144 -0
- package/src/compliance/index.js +13 -0
- package/src/compliance/pluginCompliance.js +11 -0
- package/src/deploy/FlowDeploymentClient.js +532 -0
- package/src/deploy/index.js +8 -0
- package/src/designer/FlowDesignerSession.js +158 -0
- package/src/designer/index.js +2 -0
- package/src/designer/requirements.js +184 -0
- package/src/generated/runtimeAbiLayouts.js +544 -0
- package/src/host/appHost.js +105 -0
- package/src/host/autoHost.js +113 -0
- package/src/host/browserHostAdapters.js +108 -0
- package/src/host/compiledFlowRuntimeHost.js +703 -0
- package/src/host/constants.js +55 -0
- package/src/host/dependencyRuntime.js +227 -0
- package/src/host/descriptorAbi.js +351 -0
- package/src/host/fetchService.js +237 -0
- package/src/host/httpHostAdapters.js +280 -0
- package/src/host/index.js +91 -0
- package/src/host/installedFlowHost.js +885 -0
- package/src/host/invocationAbi.js +440 -0
- package/src/host/normalize.js +372 -0
- package/src/host/packageManagers.js +369 -0
- package/src/host/profile.js +134 -0
- package/src/host/runtimeAbi.js +106 -0
- package/src/host/workspace.js +895 -0
- package/src/index.js +8 -0
- package/src/runtime/FlowRuntime.js +273 -0
- package/src/runtime/MethodRegistry.js +295 -0
- package/src/runtime/constants.js +44 -0
- package/src/runtime/index.js +19 -0
- package/src/runtime/normalize.js +377 -0
- package/src/transport/index.js +7 -0
- package/src/transport/pki.js +7 -0
- package/src/utils/crypto.js +7 -0
- package/src/utils/encoding.js +65 -0
- package/src/utils/wasmCrypto.js +69 -0
- package/tools/run-plugin-compliance-check.mjs +153 -0
package/docs/js/app.mjs
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app.mjs — Main application orchestrator for the sdn-flow IDE.
|
|
3
|
+
*
|
|
4
|
+
* Binds the flow model, canvas, Monaco editor, compilation workers,
|
|
5
|
+
* toolbar actions (import/export/CRC/compile/deploy), and VS Code
|
|
6
|
+
* webview communication.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { FlowModel, crc32Hex, canonicalBytes } from "./flow-model.mjs";
|
|
10
|
+
import { FlowCanvas } from "./flow-canvas.mjs";
|
|
11
|
+
import { EditorPanel } from "./editor-panel.mjs";
|
|
12
|
+
|
|
13
|
+
// ── VS Code webview API (if running inside VS Code) ──
|
|
14
|
+
const vscode = (typeof acquireVsCodeApi === "function") ? acquireVsCodeApi() : null;
|
|
15
|
+
|
|
16
|
+
// ── Worker RPC helper ──
|
|
17
|
+
function workerRPC(worker) {
|
|
18
|
+
let idCounter = 0;
|
|
19
|
+
const pending = new Map();
|
|
20
|
+
worker.onmessage = ({ data }) => {
|
|
21
|
+
if (data.log !== undefined) {
|
|
22
|
+
termLog(data.log, data.level || "info");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const { id, result, error } = data;
|
|
26
|
+
const p = pending.get(id);
|
|
27
|
+
if (p) {
|
|
28
|
+
pending.delete(id);
|
|
29
|
+
if (error) p.reject(new Error(error));
|
|
30
|
+
else p.resolve(result);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return (method, args) => new Promise((resolve, reject) => {
|
|
34
|
+
const id = ++idCounter;
|
|
35
|
+
pending.set(id, { resolve, reject });
|
|
36
|
+
worker.postMessage({ id, method, args });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Terminal ──
|
|
41
|
+
const termEl = document.getElementById("terminal");
|
|
42
|
+
|
|
43
|
+
function termLog(text, level = "info") {
|
|
44
|
+
const span = document.createElement("span");
|
|
45
|
+
span.className = `term-${level}`;
|
|
46
|
+
span.textContent = text + "\n";
|
|
47
|
+
termEl.appendChild(span);
|
|
48
|
+
termEl.scrollTop = termEl.scrollHeight;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function termClear() { termEl.innerHTML = ""; }
|
|
52
|
+
|
|
53
|
+
// ── Status bar ──
|
|
54
|
+
function setStatus(msg, type = "") {
|
|
55
|
+
const bar = document.getElementById("statusbar");
|
|
56
|
+
document.getElementById("status-msg").textContent = msg;
|
|
57
|
+
bar.className = type; // "", "error", "success"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateCounts() {
|
|
61
|
+
document.getElementById("node-count").textContent = `${model.nodes.size} nodes`;
|
|
62
|
+
document.getElementById("edge-count").textContent = `${model.edges.size} wires`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Model + Canvas + Editor ──
|
|
66
|
+
const model = new FlowModel();
|
|
67
|
+
const svg = document.getElementById("flow-canvas");
|
|
68
|
+
const canvas = new FlowCanvas(svg, model);
|
|
69
|
+
const editorPanel = new EditorPanel("editor-container", "editor-lang");
|
|
70
|
+
|
|
71
|
+
// ── Properties Panel ──
|
|
72
|
+
const propsBody = document.getElementById("props-body");
|
|
73
|
+
|
|
74
|
+
function renderProps(nodeId) {
|
|
75
|
+
if (!nodeId) {
|
|
76
|
+
propsBody.innerHTML = '<div class="empty-state">Select a node to edit</div>';
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const node = model.nodes.get(nodeId);
|
|
80
|
+
if (!node) return;
|
|
81
|
+
|
|
82
|
+
propsBody.innerHTML = `
|
|
83
|
+
<div class="prop-group">
|
|
84
|
+
<label class="prop-label">Label</label>
|
|
85
|
+
<input class="prop-input" data-field="label" value="${esc(node.label)}">
|
|
86
|
+
</div>
|
|
87
|
+
<div class="prop-group">
|
|
88
|
+
<label class="prop-label">Kind</label>
|
|
89
|
+
<select class="prop-select" data-field="kind">
|
|
90
|
+
${["trigger","transform","analyzer","publisher","responder","renderer","sink"]
|
|
91
|
+
.map(k => `<option value="${k}" ${k === node.kind ? "selected" : ""}>${k}</option>`).join("")}
|
|
92
|
+
</select>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="prop-group">
|
|
95
|
+
<label class="prop-label">Plugin ID</label>
|
|
96
|
+
<input class="prop-input" data-field="pluginId" value="${esc(node.pluginId)}" placeholder="com.example.plugin">
|
|
97
|
+
</div>
|
|
98
|
+
<div class="prop-group">
|
|
99
|
+
<label class="prop-label">Method ID</label>
|
|
100
|
+
<input class="prop-input" data-field="methodId" value="${esc(node.methodId)}" placeholder="process">
|
|
101
|
+
</div>
|
|
102
|
+
<div class="prop-group">
|
|
103
|
+
<label class="prop-label">Drain Policy</label>
|
|
104
|
+
<select class="prop-select" data-field="drainPolicy">
|
|
105
|
+
${["single-shot","drain-until-yield","drain-to-empty"]
|
|
106
|
+
.map(d => `<option value="${d}" ${d === node.drainPolicy ? "selected" : ""}>${d}</option>`).join("")}
|
|
107
|
+
</select>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="prop-group">
|
|
110
|
+
<label class="prop-label">Language</label>
|
|
111
|
+
<select class="prop-select" data-field="lang">
|
|
112
|
+
<option value="">None</option>
|
|
113
|
+
${["cpp","python","typescript","javascript","rust","go","c"]
|
|
114
|
+
.map(l => `<option value="${l}" ${l === node.lang ? "selected" : ""}>${l}</option>`).join("")}
|
|
115
|
+
</select>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="prop-group">
|
|
118
|
+
<label class="prop-label">Input Ports</label>
|
|
119
|
+
<div class="port-list" data-dir="input">
|
|
120
|
+
${(node.ports?.inputs || []).map(p => `
|
|
121
|
+
<div class="port-row">
|
|
122
|
+
<input class="prop-input port-name" value="${esc(p.id)}" data-port-id="${esc(p.id)}" placeholder="port id">
|
|
123
|
+
<button class="icon-btn remove-port" data-port-id="${esc(p.id)}">-</button>
|
|
124
|
+
</div>
|
|
125
|
+
`).join("")}
|
|
126
|
+
<button class="palette-btn add-port" data-dir="input">+ Add Input</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="prop-group">
|
|
130
|
+
<label class="prop-label">Output Ports</label>
|
|
131
|
+
<div class="port-list" data-dir="output">
|
|
132
|
+
${(node.ports?.outputs || []).map(p => `
|
|
133
|
+
<div class="port-row">
|
|
134
|
+
<input class="prop-input port-name" value="${esc(p.id)}" data-port-id="${esc(p.id)}" placeholder="port id">
|
|
135
|
+
<button class="icon-btn remove-port" data-port-id="${esc(p.id)}">-</button>
|
|
136
|
+
</div>
|
|
137
|
+
`).join("")}
|
|
138
|
+
<button class="palette-btn add-port" data-dir="output">+ Add Output</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
// Bind property changes
|
|
144
|
+
propsBody.querySelectorAll(".prop-input[data-field], .prop-select[data-field]").forEach(el => {
|
|
145
|
+
el.addEventListener("change", () => {
|
|
146
|
+
model.updateNode(nodeId, { [el.dataset.field]: el.value || null });
|
|
147
|
+
if (el.dataset.field === "lang") {
|
|
148
|
+
editorPanel.setNode(nodeId); // refresh editor language
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Add/remove port buttons
|
|
154
|
+
propsBody.querySelectorAll(".add-port").forEach(btn => {
|
|
155
|
+
btn.addEventListener("click", () => {
|
|
156
|
+
const dir = btn.dataset.dir;
|
|
157
|
+
const portId = prompt("Port ID:", `port-${Date.now().toString(36)}`);
|
|
158
|
+
if (portId) {
|
|
159
|
+
model.addPort(nodeId, dir, portId, portId);
|
|
160
|
+
renderProps(nodeId);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
propsBody.querySelectorAll(".remove-port").forEach(btn => {
|
|
165
|
+
btn.addEventListener("click", () => {
|
|
166
|
+
const portId = btn.dataset.portId;
|
|
167
|
+
const dir = btn.closest(".port-list").dataset.dir;
|
|
168
|
+
model.removePort(nodeId, dir, portId);
|
|
169
|
+
renderProps(nodeId);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function esc(s) { return (s || "").replace(/"/g, """).replace(/</g, "<"); }
|
|
175
|
+
|
|
176
|
+
// ── Wire canvas + editor + props ──
|
|
177
|
+
canvas.onNodeSelect((nodeId) => {
|
|
178
|
+
renderProps(nodeId);
|
|
179
|
+
updateGeneratedSource();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Double-click node → load plugin metadata from manifest
|
|
183
|
+
canvas.onNodeDblClick(async (nodeId) => {
|
|
184
|
+
const node = model.nodes.get(nodeId);
|
|
185
|
+
if (!node) return;
|
|
186
|
+
if (!node.pluginId) {
|
|
187
|
+
termLog(`Node ${node.label}: no pluginId set, cannot load metadata.`, "warn");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
termLog(`Loading metadata for plugin: ${node.pluginId}...`, "info");
|
|
191
|
+
|
|
192
|
+
// Try fetching manifest from examples
|
|
193
|
+
const manifests = [
|
|
194
|
+
`../examples/plugins/${node.pluginId.split(".").pop()}/manifest.json`,
|
|
195
|
+
`../examples/plugins/${node.methodId}/manifest.json`,
|
|
196
|
+
];
|
|
197
|
+
let found = false;
|
|
198
|
+
for (const url of manifests) {
|
|
199
|
+
try {
|
|
200
|
+
const resp = await fetch(url);
|
|
201
|
+
if (resp.ok) {
|
|
202
|
+
const manifest = await resp.json();
|
|
203
|
+
termLog(`Plugin manifest loaded: ${manifest.pluginId || node.pluginId}`, "success");
|
|
204
|
+
termLog(JSON.stringify(manifest, null, 2), "info");
|
|
205
|
+
// Update node with manifest data
|
|
206
|
+
if (manifest.methods) {
|
|
207
|
+
const methods = Array.isArray(manifest.methods) ? manifest.methods : Object.values(manifest.methods);
|
|
208
|
+
const method = methods.find(m => m.methodId === node.methodId) || methods[0];
|
|
209
|
+
if (method) {
|
|
210
|
+
const ports = { inputs: [], outputs: [] };
|
|
211
|
+
if (method.inputs) method.inputs.forEach(p => ports.inputs.push({ id: p.portId || p.id, label: p.label || p.portId || p.id }));
|
|
212
|
+
if (method.outputs) method.outputs.forEach(p => ports.outputs.push({ id: p.portId || p.id, label: p.label || p.portId || p.id }));
|
|
213
|
+
if (ports.inputs.length || ports.outputs.length) {
|
|
214
|
+
model.updateNode(nodeId, { ports });
|
|
215
|
+
termLog(` Updated ports from manifest`, "success");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
found = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
} catch (e) { /* try next */ }
|
|
223
|
+
}
|
|
224
|
+
if (!found) {
|
|
225
|
+
// Show what we know in the terminal
|
|
226
|
+
termLog(` pluginId: ${node.pluginId}`, "info");
|
|
227
|
+
termLog(` methodId: ${node.methodId}`, "info");
|
|
228
|
+
termLog(` kind: ${node.kind}`, "info");
|
|
229
|
+
termLog(` drainPolicy: ${node.drainPolicy}`, "info");
|
|
230
|
+
termLog(` (manifest not found locally — set up artifact catalog for remote resolution)`, "warn");
|
|
231
|
+
}
|
|
232
|
+
renderProps(nodeId);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
canvas.onEdgeSelect((edgeId) => {
|
|
236
|
+
renderProps(null);
|
|
237
|
+
if (edgeId) {
|
|
238
|
+
const edge = model.edges.get(edgeId);
|
|
239
|
+
if (edge) {
|
|
240
|
+
propsBody.innerHTML = `
|
|
241
|
+
<div class="prop-group"><label class="prop-label">Edge</label><span style="color:#ccc">${edge.edgeId}</span></div>
|
|
242
|
+
<div class="prop-group"><label class="prop-label">From</label><span style="color:#ccc">${edge.fromNodeId}:${edge.fromPortId}</span></div>
|
|
243
|
+
<div class="prop-group"><label class="prop-label">To</label><span style="color:#ccc">${edge.toNodeId}:${edge.toPortId}</span></div>
|
|
244
|
+
<div class="prop-group">
|
|
245
|
+
<label class="prop-label">Backpressure</label>
|
|
246
|
+
<select class="prop-select" id="edge-bp">
|
|
247
|
+
${["drop","latest","queue","block-request","coalesce","drain-to-empty"]
|
|
248
|
+
.map(b => `<option value="${b}" ${b === edge.backpressurePolicy ? "selected" : ""}>${b}</option>`).join("")}
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="prop-group">
|
|
252
|
+
<label class="prop-label">Queue Depth</label>
|
|
253
|
+
<input class="prop-input" id="edge-qd" type="number" value="${edge.queueDepth}">
|
|
254
|
+
</div>
|
|
255
|
+
`;
|
|
256
|
+
document.getElementById("edge-bp")?.addEventListener("change", (e) => { edge.backpressurePolicy = e.target.value; });
|
|
257
|
+
document.getElementById("edge-qd")?.addEventListener("change", (e) => { edge.queueDepth = parseInt(e.target.value) || 32; });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
model.addEventListener("change", () => {
|
|
263
|
+
updateCounts();
|
|
264
|
+
// Update CRC badge
|
|
265
|
+
const crc = model.computeCRC();
|
|
266
|
+
document.getElementById("crc-badge").textContent = crc;
|
|
267
|
+
updateGeneratedSource();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ── Generated Source (read-only view of the compiled flow) ──
|
|
271
|
+
|
|
272
|
+
function updateGeneratedSource() {
|
|
273
|
+
const json = model.toJSON();
|
|
274
|
+
const nodes = json.nodes || [];
|
|
275
|
+
const edges = json.edges || [];
|
|
276
|
+
const triggers = json.triggers || [];
|
|
277
|
+
|
|
278
|
+
// Generate a C++ source preview of the flow runtime
|
|
279
|
+
const includes = [
|
|
280
|
+
'#include <cstdint>',
|
|
281
|
+
'#include <cstring>',
|
|
282
|
+
'#include "flatbuffers/flatbuffers.h"',
|
|
283
|
+
'#include "flow_manifest_generated.h"',
|
|
284
|
+
'',
|
|
285
|
+
'// ═══════════════════════════════════════════════════',
|
|
286
|
+
`// Generated flow: ${json.name || "Untitled"}`,
|
|
287
|
+
`// Program ID: ${json.programId || "(none)"}`,
|
|
288
|
+
`// Nodes: ${nodes.length} Edges: ${edges.length} Triggers: ${triggers.length}`,
|
|
289
|
+
`// CRC-32: ${model.computeCRC()}`,
|
|
290
|
+
'// ═══════════════════════════════════════════════════',
|
|
291
|
+
'',
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
// Manifest embed
|
|
295
|
+
const manifest = [
|
|
296
|
+
'// ── Embedded FlatBuffer manifest ──',
|
|
297
|
+
'static const uint8_t FLOW_MANIFEST[] = { /* built at compile time */ };',
|
|
298
|
+
'static const uint32_t FLOW_MANIFEST_SIZE = sizeof(FLOW_MANIFEST);',
|
|
299
|
+
'',
|
|
300
|
+
'extern "C" const uint8_t* flow_get_manifest_flatbuffer() { return FLOW_MANIFEST; }',
|
|
301
|
+
'extern "C" uint32_t flow_get_manifest_flatbuffer_size() { return FLOW_MANIFEST_SIZE; }',
|
|
302
|
+
'',
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
// Plugin artifact imports
|
|
306
|
+
const plugins = [...new Set(nodes.map(n => n.pluginId).filter(Boolean))];
|
|
307
|
+
const pluginDecls = plugins.length > 0 ? [
|
|
308
|
+
'// ── Plugin artifact imports ──',
|
|
309
|
+
...plugins.map((p, i) => `static const uint8_t* PLUGIN_${i}_WASM = nullptr; // ${p}`),
|
|
310
|
+
...plugins.map((p, i) => `static uint32_t PLUGIN_${i}_SIZE = 0;`),
|
|
311
|
+
'',
|
|
312
|
+
] : [];
|
|
313
|
+
|
|
314
|
+
// Node declarations
|
|
315
|
+
const nodeDecls = [
|
|
316
|
+
'// ── Node declarations ──',
|
|
317
|
+
...nodes.map(n => {
|
|
318
|
+
const kind = n.kind.toUpperCase();
|
|
319
|
+
return `// [${kind}] ${n.label} (${n.pluginId || "inline"} :: ${n.methodId || "process"})`;
|
|
320
|
+
}),
|
|
321
|
+
'',
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
// Topology (edges)
|
|
325
|
+
const topo = edges.length > 0 ? [
|
|
326
|
+
'// ── Topology ──',
|
|
327
|
+
...edges.map(e =>
|
|
328
|
+
`// ${e.fromNodeId}:${e.fromPortId} ──► ${e.toNodeId}:${e.toPortId} [${e.backpressurePolicy}, depth=${e.queueDepth}]`
|
|
329
|
+
),
|
|
330
|
+
'',
|
|
331
|
+
] : [];
|
|
332
|
+
|
|
333
|
+
// Trigger bindings
|
|
334
|
+
const trigBindings = (json.triggerBindings || []).length > 0 ? [
|
|
335
|
+
'// ── Trigger bindings ──',
|
|
336
|
+
...(json.triggerBindings || []).map(b =>
|
|
337
|
+
`// ${b.triggerId} ──► ${b.targetNodeId}:${b.targetPortId} [${b.backpressurePolicy}]`
|
|
338
|
+
),
|
|
339
|
+
'',
|
|
340
|
+
] : [];
|
|
341
|
+
|
|
342
|
+
// Main entry
|
|
343
|
+
const main = [
|
|
344
|
+
'// ── Runtime entry ──',
|
|
345
|
+
'extern "C" int flow_init() {',
|
|
346
|
+
' // Initialize node state, wire topology, register triggers',
|
|
347
|
+
...nodes.map(n => ` // init_node("${n.nodeId}", ${n.kind});`),
|
|
348
|
+
...edges.map(e => ` // wire("${e.fromNodeId}:${e.fromPortId}", "${e.toNodeId}:${e.toPortId}");`),
|
|
349
|
+
' return 0;',
|
|
350
|
+
'}',
|
|
351
|
+
'',
|
|
352
|
+
'extern "C" int flow_step(const uint8_t* frame, uint32_t len) {',
|
|
353
|
+
' // Dispatch frame through topology',
|
|
354
|
+
' return 0;',
|
|
355
|
+
'}',
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const source = [...includes, ...manifest, ...pluginDecls, ...nodeDecls, ...topo, ...trigBindings, ...main].join('\n');
|
|
359
|
+
|
|
360
|
+
editorPanel.setGeneratedSource(source);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Palette drag ──
|
|
364
|
+
document.querySelectorAll(".palette-item[draggable]").forEach(item => {
|
|
365
|
+
item.addEventListener("dragstart", (e) => {
|
|
366
|
+
e.dataTransfer.setData("text/x-sdn-kind", item.dataset.kind);
|
|
367
|
+
e.dataTransfer.setData("text/x-sdn-lang", item.dataset.lang || "");
|
|
368
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ── Toolbar: Import ──
|
|
373
|
+
document.getElementById("btn-import").addEventListener("click", async () => {
|
|
374
|
+
if (vscode) {
|
|
375
|
+
// VS Code: request file from extension host
|
|
376
|
+
vscode.postMessage({ command: "importFlow" });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const input = document.createElement("input");
|
|
380
|
+
input.type = "file";
|
|
381
|
+
input.accept = ".json";
|
|
382
|
+
input.onchange = async () => {
|
|
383
|
+
const file = input.files[0];
|
|
384
|
+
if (!file) return;
|
|
385
|
+
try {
|
|
386
|
+
const text = await file.text();
|
|
387
|
+
const json = JSON.parse(text);
|
|
388
|
+
model.fromJSON(json);
|
|
389
|
+
setStatus(`Loaded: ${json.name || file.name}`, "success");
|
|
390
|
+
termLog(`Imported flow: ${json.name || file.name}`, "success");
|
|
391
|
+
} catch (err) {
|
|
392
|
+
setStatus("Import failed", "error");
|
|
393
|
+
termLog(`Import error: ${err.message}`, "error");
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
input.click();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ── Toolbar: Export ──
|
|
400
|
+
document.getElementById("btn-export").addEventListener("click", () => {
|
|
401
|
+
const json = model.toJSON();
|
|
402
|
+
const text = JSON.stringify(json, null, 2);
|
|
403
|
+
|
|
404
|
+
if (vscode) {
|
|
405
|
+
vscode.postMessage({ command: "exportFlow", data: text });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const blob = new Blob([text], { type: "application/json" });
|
|
409
|
+
const url = URL.createObjectURL(blob);
|
|
410
|
+
const a = document.createElement("a");
|
|
411
|
+
a.href = url;
|
|
412
|
+
a.download = `${json.name || "flow"}.json`;
|
|
413
|
+
a.click();
|
|
414
|
+
URL.revokeObjectURL(url);
|
|
415
|
+
termLog(`Exported: ${a.download}`, "success");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ── Toolbar: CRC ──
|
|
419
|
+
document.getElementById("btn-crc").addEventListener("click", async () => {
|
|
420
|
+
const crc = model.computeCRC();
|
|
421
|
+
const sha = await model.computeSHA256();
|
|
422
|
+
termLog(`CRC-32: ${crc}`, "info");
|
|
423
|
+
termLog(`SHA-256: ${sha}`, "info");
|
|
424
|
+
setStatus(`CRC: ${crc}`);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ── Compilation Workers ──
|
|
428
|
+
let emceptionWorker = null;
|
|
429
|
+
let emceptionRPC = null;
|
|
430
|
+
let emceptionReady = false;
|
|
431
|
+
|
|
432
|
+
let pyodideWorker = null;
|
|
433
|
+
let pyodideRPC = null;
|
|
434
|
+
let pyodideReady = false;
|
|
435
|
+
|
|
436
|
+
// Resolve worker URLs relative to this module, not the page
|
|
437
|
+
const WORKER_BASE = new URL("./workers/", import.meta.url).href;
|
|
438
|
+
|
|
439
|
+
// Emception base URL — override via ?emception=<url> query param
|
|
440
|
+
const EMCEPTION_BASE = new URLSearchParams(location.search).get("emception") || "https://digitalarsenal.github.io/emception/";
|
|
441
|
+
|
|
442
|
+
async function ensureEmception() {
|
|
443
|
+
if (emceptionReady) return;
|
|
444
|
+
if (!emceptionWorker) {
|
|
445
|
+
emceptionWorker = new Worker(WORKER_BASE + "emception.worker.js");
|
|
446
|
+
emceptionRPC = workerRPC(emceptionWorker);
|
|
447
|
+
}
|
|
448
|
+
setStatus("Loading emception...");
|
|
449
|
+
await emceptionRPC("init", { baseUrl: EMCEPTION_BASE });
|
|
450
|
+
emceptionReady = true;
|
|
451
|
+
setStatus("Emception ready", "success");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function ensurePyodide() {
|
|
455
|
+
if (pyodideReady) return;
|
|
456
|
+
if (!pyodideWorker) {
|
|
457
|
+
pyodideWorker = new Worker(WORKER_BASE + "pyodide.worker.js");
|
|
458
|
+
pyodideRPC = workerRPC(pyodideWorker);
|
|
459
|
+
}
|
|
460
|
+
setStatus("Loading Pyodide...");
|
|
461
|
+
await pyodideRPC("init");
|
|
462
|
+
pyodideReady = true;
|
|
463
|
+
setStatus("Pyodide ready", "success");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Toolbar: Compile ──
|
|
467
|
+
document.getElementById("btn-compile").addEventListener("click", async () => {
|
|
468
|
+
const nodes = [...model.nodes.values()];
|
|
469
|
+
const cppNodes = nodes.filter(n => n.lang === "cpp" || n.lang === "c");
|
|
470
|
+
const pyNodes = nodes.filter(n => n.lang === "python");
|
|
471
|
+
|
|
472
|
+
if (cppNodes.length === 0 && pyNodes.length === 0) {
|
|
473
|
+
termLog("No compilable nodes (add C++ or Python modules).", "warn");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
termLog("=== Compile started ===", "info");
|
|
478
|
+
setStatus("Compiling...");
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// Compile C++ nodes via emception
|
|
482
|
+
if (cppNodes.length > 0) {
|
|
483
|
+
await ensureEmception();
|
|
484
|
+
for (const node of cppNodes) {
|
|
485
|
+
termLog(`Compiling ${node.label} (${node.lang})...`, "info");
|
|
486
|
+
const result = await emceptionRPC("compile", {
|
|
487
|
+
source: node.source,
|
|
488
|
+
lang: node.lang,
|
|
489
|
+
flags: ["-O2", "-std=c++20", "-sWASM=1"],
|
|
490
|
+
outputName: node.nodeId,
|
|
491
|
+
});
|
|
492
|
+
if (result.returncode === 0) {
|
|
493
|
+
termLog(` ${node.label}: OK`, "success");
|
|
494
|
+
// Set node status
|
|
495
|
+
const el = canvas.nodeElements.get(node.nodeId);
|
|
496
|
+
el?.querySelector(".node-status")?.classList.add("success");
|
|
497
|
+
} else {
|
|
498
|
+
termLog(` ${node.label}: FAILED (exit ${result.returncode})`, "error");
|
|
499
|
+
if (result.stderr) termLog(result.stderr, "error");
|
|
500
|
+
const el = canvas.nodeElements.get(node.nodeId);
|
|
501
|
+
el?.querySelector(".node-status")?.classList.add("error");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Run Python nodes via Pyodide
|
|
507
|
+
if (pyNodes.length > 0) {
|
|
508
|
+
await ensurePyodide();
|
|
509
|
+
for (const node of pyNodes) {
|
|
510
|
+
termLog(`Running ${node.label} (Python)...`, "info");
|
|
511
|
+
const result = await pyodideRPC("run", { code: node.source });
|
|
512
|
+
termLog(` ${node.label}: OK`, "success");
|
|
513
|
+
if (result.stdout) termLog(result.stdout, "info");
|
|
514
|
+
const el = canvas.nodeElements.get(node.nodeId);
|
|
515
|
+
el?.querySelector(".node-status")?.classList.add("success");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Compute final artifact hash
|
|
520
|
+
const sha = await model.computeSHA256();
|
|
521
|
+
termLog(`Graph SHA-256: ${sha}`, "info");
|
|
522
|
+
setStatus("Compile complete", "success");
|
|
523
|
+
termLog("=== Compile finished ===", "success");
|
|
524
|
+
} catch (err) {
|
|
525
|
+
termLog(`Compile error: ${err.message}`, "error");
|
|
526
|
+
setStatus("Compile failed", "error");
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// ── Toolbar: Deploy ──
|
|
531
|
+
document.getElementById("btn-deploy").addEventListener("click", async () => {
|
|
532
|
+
termLog("=== Deploy pipeline started ===", "info");
|
|
533
|
+
setStatus("Deploying...");
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
// Step 1: Compile (reuse compile logic)
|
|
537
|
+
document.getElementById("btn-compile").click();
|
|
538
|
+
|
|
539
|
+
// Step 2: Compute integrity
|
|
540
|
+
const crc = model.computeCRC();
|
|
541
|
+
const sha = await model.computeSHA256();
|
|
542
|
+
termLog(`Artifact CRC-32: ${crc}`, "info");
|
|
543
|
+
termLog(`Artifact SHA-256: ${sha}`, "info");
|
|
544
|
+
|
|
545
|
+
// Step 3: Sign with HD-wallet (if wallet is available)
|
|
546
|
+
const flowJson = model.toJSON();
|
|
547
|
+
const payload = canonicalBytes(flowJson);
|
|
548
|
+
|
|
549
|
+
termLog("Preparing deployment authorization...", "info");
|
|
550
|
+
termLog(` programId: ${flowJson.programId || "(unnamed)"}`, "info");
|
|
551
|
+
termLog(` graphHash: ${sha}`, "info");
|
|
552
|
+
termLog(` nodes: ${flowJson.nodes.length}, edges: ${flowJson.edges.length}`, "info");
|
|
553
|
+
|
|
554
|
+
// The actual signing would integrate with hd-wallet-wasm:
|
|
555
|
+
// const wallet = await initHDWallet();
|
|
556
|
+
// const master = wallet.hdkey.fromSeed(seed);
|
|
557
|
+
// const signingKey = getSigningKey(master, WellKnownCoinType.SDN);
|
|
558
|
+
// const digest = wallet.utils.sha256(payload);
|
|
559
|
+
// const signature = wallet.curves.secp256k1.sign(digest, signingKey.privateKey());
|
|
560
|
+
//
|
|
561
|
+
// For now, log the authorization shape:
|
|
562
|
+
termLog(" [wallet integration point: sign authorization envelope]", "warn");
|
|
563
|
+
termLog(" [wallet integration point: encrypt with recipient public key]", "warn");
|
|
564
|
+
|
|
565
|
+
// Step 4: Package
|
|
566
|
+
const deployment = {
|
|
567
|
+
version: 1,
|
|
568
|
+
encrypted: false,
|
|
569
|
+
payload: {
|
|
570
|
+
version: 1,
|
|
571
|
+
kind: "compiled-flow-wasm-deployment",
|
|
572
|
+
artifact: {
|
|
573
|
+
artifactId: `flow-${Date.now().toString(36)}`,
|
|
574
|
+
programId: flowJson.programId,
|
|
575
|
+
graphHash: sha,
|
|
576
|
+
manifestHash: null,
|
|
577
|
+
abiVersion: 1,
|
|
578
|
+
},
|
|
579
|
+
authorization: null, // would be signed envelope
|
|
580
|
+
target: null,
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
if (vscode) {
|
|
585
|
+
// In VS Code, send to extension host to save
|
|
586
|
+
vscode.postMessage({ command: "deploy", data: JSON.stringify(deployment, null, 2) });
|
|
587
|
+
termLog("Deployment package sent to VS Code host.", "success");
|
|
588
|
+
} else {
|
|
589
|
+
// In browser, offer download
|
|
590
|
+
const blob = new Blob([JSON.stringify(deployment, null, 2)], { type: "application/json" });
|
|
591
|
+
const url = URL.createObjectURL(blob);
|
|
592
|
+
const a = document.createElement("a");
|
|
593
|
+
a.href = url;
|
|
594
|
+
a.download = `deployment-${deployment.payload.artifact.artifactId}.json`;
|
|
595
|
+
a.click();
|
|
596
|
+
URL.revokeObjectURL(url);
|
|
597
|
+
termLog(`Deployment package downloaded: ${a.download}`, "success");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
setStatus("Deploy complete", "success");
|
|
601
|
+
termLog("=== Deploy pipeline finished ===", "success");
|
|
602
|
+
} catch (err) {
|
|
603
|
+
termLog(`Deploy error: ${err.message}`, "error");
|
|
604
|
+
setStatus("Deploy failed", "error");
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ── Toolbar: Wallet ──
|
|
609
|
+
document.getElementById("btn-wallet").addEventListener("click", () => {
|
|
610
|
+
document.getElementById("wallet-dialog").showModal();
|
|
611
|
+
});
|
|
612
|
+
document.getElementById("btn-close-wallet").addEventListener("click", () => {
|
|
613
|
+
document.getElementById("wallet-dialog").close();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ── Toolbar: Delete node ──
|
|
617
|
+
document.getElementById("btn-delete-node").addEventListener("click", () => {
|
|
618
|
+
for (const nodeId of [...canvas.selectedNodes]) {
|
|
619
|
+
model.removeNode(nodeId);
|
|
620
|
+
}
|
|
621
|
+
canvas.selectedNodes.clear();
|
|
622
|
+
renderProps(null);
|
|
623
|
+
editorPanel.setNode(null);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ── Terminal: Clear ──
|
|
627
|
+
document.getElementById("btn-clear-term").addEventListener("click", termClear);
|
|
628
|
+
|
|
629
|
+
// ── Template: ISS Proximity OEM ──
|
|
630
|
+
document.getElementById("btn-load-iss").addEventListener("click", async () => {
|
|
631
|
+
try {
|
|
632
|
+
const resp = await fetch("../examples/flows/iss-proximity-oem/flow.json");
|
|
633
|
+
if (!resp.ok) {
|
|
634
|
+
// Try embedded example
|
|
635
|
+
loadISSExample();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const json = await resp.json();
|
|
639
|
+
model.fromJSON(json);
|
|
640
|
+
setStatus(`Loaded: ${json.name}`, "success");
|
|
641
|
+
termLog(`Loaded template: ${json.name}`, "success");
|
|
642
|
+
} catch (e) {
|
|
643
|
+
loadISSExample();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
function loadISSExample() {
|
|
648
|
+
// Embedded minimal version of ISS Proximity flow
|
|
649
|
+
model.fromJSON({
|
|
650
|
+
programId: "com.digitalarsenal.examples.iss-proximity-oem",
|
|
651
|
+
name: "ISS Proximity OEM Flow",
|
|
652
|
+
version: "0.1.0",
|
|
653
|
+
description: "Stream OMMs, query proximity, propagate, generate OEMs.",
|
|
654
|
+
nodes: [
|
|
655
|
+
{ nodeId: "db-ingest", pluginId: "com.digitalarsenal.flatsql.store", methodId: "upsert_records", kind: "transform", drainPolicy: "drain-until-yield", label: "DB Ingest", ports: { inputs: [{ id: "records", label: "records" }], outputs: [{ id: "out", label: "out" }] } },
|
|
656
|
+
{ nodeId: "build-query", pluginId: "com.digitalarsenal.flow.query-anchor", methodId: "build_radius_query", kind: "analyzer", drainPolicy: "drain-until-yield", label: "Build Query", ports: { inputs: [{ id: "tick", label: "tick" }], outputs: [{ id: "query", label: "query" }] } },
|
|
657
|
+
{ nodeId: "db-query", pluginId: "com.digitalarsenal.flatsql.store", methodId: "query_objects_within_radius", kind: "analyzer", drainPolicy: "drain-until-yield", label: "DB Query", ports: { inputs: [{ id: "query", label: "query" }], outputs: [{ id: "matches", label: "matches" }] } },
|
|
658
|
+
{ nodeId: "propagate", pluginId: "com.digitalarsenal.propagator.sgp4", methodId: "propagate_one_orbit_samples", kind: "transform", drainPolicy: "drain-until-yield", label: "Propagate", ports: { inputs: [{ id: "selection", label: "selection" }], outputs: [{ id: "samples", label: "samples" }] } },
|
|
659
|
+
{ nodeId: "generate-oem", pluginId: "com.digitalarsenal.oem.generator", methodId: "generate_oem", kind: "transform", drainPolicy: "drain-until-yield", label: "Gen OEM", ports: { inputs: [{ id: "samples", label: "samples" }], outputs: [{ id: "oems", label: "oems" }] } },
|
|
660
|
+
{ nodeId: "write-oem", pluginId: "com.digitalarsenal.oem.file-writer", methodId: "write_oem_files", kind: "sink", drainPolicy: "drain-until-yield", label: "Write OEM", ports: { inputs: [{ id: "oems", label: "oems" }], outputs: [] } },
|
|
661
|
+
{ nodeId: "publish-oem", pluginId: "com.digitalarsenal.oem.publisher", methodId: "publish_oem", kind: "publisher", drainPolicy: "drain-until-yield", label: "Publish OEM", ports: { inputs: [{ id: "oems", label: "oems" }], outputs: [] } },
|
|
662
|
+
],
|
|
663
|
+
edges: [
|
|
664
|
+
{ edgeId: "e1", fromNodeId: "build-query", fromPortId: "query", toNodeId: "db-query", toPortId: "query", backpressurePolicy: "latest", queueDepth: 1 },
|
|
665
|
+
{ edgeId: "e2", fromNodeId: "db-query", fromPortId: "matches", toNodeId: "propagate", toPortId: "selection", backpressurePolicy: "queue", queueDepth: 32 },
|
|
666
|
+
{ edgeId: "e3", fromNodeId: "propagate", fromPortId: "samples", toNodeId: "generate-oem", toPortId: "samples", backpressurePolicy: "queue", queueDepth: 32 },
|
|
667
|
+
{ edgeId: "e4", fromNodeId: "generate-oem", fromPortId: "oems", toNodeId: "write-oem", toPortId: "oems", backpressurePolicy: "queue", queueDepth: 32 },
|
|
668
|
+
{ edgeId: "e5", fromNodeId: "generate-oem", fromPortId: "oems", toNodeId: "publish-oem", toPortId: "oems", backpressurePolicy: "queue", queueDepth: 32 },
|
|
669
|
+
],
|
|
670
|
+
triggers: [
|
|
671
|
+
{ triggerId: "omm-subscription", kind: "pubsub-subscription", source: "/sdn/catalog/omm" },
|
|
672
|
+
{ triggerId: "refresh-query", kind: "timer", source: "refresh-query", defaultIntervalMs: 15000 },
|
|
673
|
+
],
|
|
674
|
+
triggerBindings: [
|
|
675
|
+
{ triggerId: "omm-subscription", targetNodeId: "db-ingest", targetPortId: "records", backpressurePolicy: "queue", queueDepth: 4096 },
|
|
676
|
+
{ triggerId: "refresh-query", targetNodeId: "build-query", targetPortId: "tick", backpressurePolicy: "latest", queueDepth: 1 },
|
|
677
|
+
],
|
|
678
|
+
editor: {
|
|
679
|
+
viewport: { x: 0, y: 0, zoom: 0.85 },
|
|
680
|
+
nodes: {
|
|
681
|
+
"db-ingest": { x: 80, y: 160 },
|
|
682
|
+
"build-query": { x: 80, y: 380 },
|
|
683
|
+
"db-query": { x: 360, y: 380 },
|
|
684
|
+
"propagate": { x: 640, y: 380 },
|
|
685
|
+
"generate-oem": { x: 920, y: 380 },
|
|
686
|
+
"write-oem": { x: 1200, y: 280 },
|
|
687
|
+
"publish-oem": { x: 1200, y: 480 },
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
setStatus("Loaded: ISS Proximity OEM Flow", "success");
|
|
692
|
+
termLog("Loaded template: ISS Proximity OEM Flow", "success");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── Resize handles ──
|
|
696
|
+
function setupResize(handleId, direction, getTarget) {
|
|
697
|
+
const handle = document.getElementById(handleId);
|
|
698
|
+
if (!handle) return;
|
|
699
|
+
handle.addEventListener("mousedown", (e) => {
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
const target = getTarget();
|
|
702
|
+
const startX = e.clientX;
|
|
703
|
+
const startY = e.clientY;
|
|
704
|
+
const startW = target.offsetWidth;
|
|
705
|
+
const startH = target.offsetHeight;
|
|
706
|
+
const onMove = (e2) => {
|
|
707
|
+
if (direction === "horizontal") {
|
|
708
|
+
const newW = startW - (e2.clientX - startX);
|
|
709
|
+
target.style.width = `${Math.max(200, newW)}px`;
|
|
710
|
+
} else {
|
|
711
|
+
const newH = startH + (e2.clientY - startY);
|
|
712
|
+
target.style.height = `${Math.max(60, newH)}px`;
|
|
713
|
+
}
|
|
714
|
+
editorPanel.layout();
|
|
715
|
+
};
|
|
716
|
+
const onUp = () => {
|
|
717
|
+
window.removeEventListener("mousemove", onMove);
|
|
718
|
+
window.removeEventListener("mouseup", onUp);
|
|
719
|
+
};
|
|
720
|
+
window.addEventListener("mousemove", onMove);
|
|
721
|
+
window.addEventListener("mouseup", onUp);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
setupResize("right-resize", "horizontal", () => document.getElementById("right-panel"));
|
|
726
|
+
|
|
727
|
+
// ── VS Code message handler ──
|
|
728
|
+
if (vscode) {
|
|
729
|
+
window.addEventListener("message", ({ data }) => {
|
|
730
|
+
if (data.command === "loadFlow") {
|
|
731
|
+
try {
|
|
732
|
+
model.fromJSON(JSON.parse(data.data));
|
|
733
|
+
setStatus("Flow loaded from VS Code", "success");
|
|
734
|
+
} catch (e) {
|
|
735
|
+
termLog(`VS Code load error: ${e.message}`, "error");
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ── Init ──
|
|
742
|
+
async function init() {
|
|
743
|
+
await editorPanel.ready();
|
|
744
|
+
editorPanel.bindModel(model);
|
|
745
|
+
updateCounts();
|
|
746
|
+
setStatus("Ready");
|
|
747
|
+
termLog("sdn-flow IDE ready.", "success");
|
|
748
|
+
termLog("Drag nodes from the palette, or load the ISS Proximity template.", "info");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
init();
|