@particle-academy/fancy-flow 0.65.1 → 0.66.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/README.md +100 -3
- package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
- package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
- package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
- package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
- package/dist/chunk-2QXKDLGT.js +58 -0
- package/dist/chunk-2QXKDLGT.js.map +1 -0
- package/dist/{chunk-77V4QC6Y.js → chunk-B7HKJJVV.js} +3 -3
- package/dist/{chunk-77V4QC6Y.js.map → chunk-B7HKJJVV.js.map} +1 -1
- package/dist/{chunk-MBVX4ZRB.js → chunk-FNYLKSFJ.js} +3 -3
- package/dist/{chunk-MBVX4ZRB.js.map → chunk-FNYLKSFJ.js.map} +1 -1
- package/dist/{chunk-JF6WCRBU.js → chunk-HQGOFGAL.js} +861 -40
- package/dist/chunk-HQGOFGAL.js.map +1 -0
- package/dist/{chunk-5H54OTKT.js → chunk-IEYCNFXZ.js} +3 -3
- package/dist/{chunk-5H54OTKT.js.map → chunk-IEYCNFXZ.js.map} +1 -1
- package/dist/{chunk-W5DPKJY4.js → chunk-MTONBM53.js} +4 -4
- package/dist/chunk-MTONBM53.js.map +1 -0
- package/dist/{chunk-RIAFHQT5.js → chunk-PNHZHEWE.js} +3 -3
- package/dist/{chunk-RIAFHQT5.js.map → chunk-PNHZHEWE.js.map} +1 -1
- package/dist/{chunk-EO6444T2.js → chunk-UCCTXJXL.js} +4 -4
- package/dist/{chunk-EO6444T2.js.map → chunk-UCCTXJXL.js.map} +1 -1
- package/dist/connectors.d.cts +2 -2
- package/dist/connectors.d.ts +2 -2
- package/dist/durable/index.d.cts +2 -2
- package/dist/durable/index.d.ts +2 -2
- package/dist/durable.cjs +837 -47
- package/dist/durable.cjs.map +1 -1
- package/dist/durable.js +2 -3
- package/dist/durable.js.map +1 -1
- package/dist/engine.cjs +843 -132
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +6 -7
- package/dist/engine.d.ts +6 -7
- package/dist/engine.js +5 -6
- package/dist/engine.js.map +1 -1
- package/dist/fields/react-fancy.d.cts +3 -3
- package/dist/fields/react-fancy.d.ts +3 -3
- package/dist/index.cjs +853 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -36
- package/dist/index.d.ts +65 -36
- package/dist/index.js +19 -18
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.cts +1 -1
- package/dist/layout/index.d.ts +1 -1
- package/dist/llm/prism.cjs.map +1 -1
- package/dist/llm/prism.d.cts +1 -2
- package/dist/llm/prism.d.ts +1 -2
- package/dist/llm/prism.js +1 -1
- package/dist/llm/vercel-ai.cjs.map +1 -1
- package/dist/llm/vercel-ai.d.cts +1 -2
- package/dist/llm/vercel-ai.d.ts +1 -2
- package/dist/llm/vercel-ai.js +1 -1
- package/dist/registry/index.d.cts +5 -6
- package/dist/registry/index.d.ts +5 -6
- package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
- package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
- package/dist/registry.cjs +867 -37
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
- package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
- package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
- package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
- package/dist/runtime/index.d.cts +5 -5
- package/dist/runtime/index.d.ts +5 -5
- package/dist/runtime.cjs +837 -36
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +4 -4
- package/dist/schema/index.d.cts +19 -6
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema.cjs +837 -36
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +4 -4
- package/dist/screens.cjs +837 -36
- package/dist/screens.cjs.map +1 -1
- package/dist/screens.d.cts +2 -2
- package/dist/screens.d.ts +2 -2
- package/dist/screens.js +5 -5
- package/dist/terminal/fancy-term-host.cjs +99 -0
- package/dist/terminal/fancy-term-host.cjs.map +1 -0
- package/dist/terminal/fancy-term-host.d.cts +76 -0
- package/dist/terminal/fancy-term-host.d.ts +76 -0
- package/dist/terminal/fancy-term-host.js +92 -0
- package/dist/terminal/fancy-term-host.js.map +1 -0
- package/dist/types-B-Syk9-M.d.cts +620 -0
- package/dist/types-B-Syk9-M.d.ts +620 -0
- package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
- package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
- package/dist/ux.cjs +845 -39
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.d.cts +14 -2
- package/dist/ux.d.ts +14 -2
- package/dist/ux.js +10 -5
- package/dist/ux.js.map +1 -1
- package/package.json +11 -1
- package/dist/capabilities-BYa5p5jw.d.cts +0 -112
- package/dist/capabilities-COOXRiNL.d.ts +0 -112
- package/dist/chunk-JF6WCRBU.js.map +0 -1
- package/dist/chunk-UM4C46AF.js +0 -103
- package/dist/chunk-UM4C46AF.js.map +0 -1
- package/dist/chunk-USL4FMFU.js +0 -41
- package/dist/chunk-USL4FMFU.js.map +0 -1
- package/dist/chunk-W5DPKJY4.js.map +0 -1
- package/dist/types-JFYjPJAG.d.cts +0 -333
- package/dist/types-JFYjPJAG.d.ts +0 -333
package/dist/engine.cjs
CHANGED
|
@@ -2533,9 +2533,9 @@ function transition_default() {
|
|
|
2533
2533
|
// node_modules/d3-transition/src/transition/end.js
|
|
2534
2534
|
function end_default() {
|
|
2535
2535
|
var on0, on1, that = this, id2 = that._id, size = that.size();
|
|
2536
|
-
return new Promise(function(
|
|
2536
|
+
return new Promise(function(resolve2, reject) {
|
|
2537
2537
|
var cancel = { value: reject }, end = { value: function() {
|
|
2538
|
-
if (--size === 0)
|
|
2538
|
+
if (--size === 0) resolve2();
|
|
2539
2539
|
} };
|
|
2540
2540
|
that.each(function() {
|
|
2541
2541
|
var schedule = set2(this, id2), on = schedule.on;
|
|
@@ -2547,7 +2547,7 @@ function end_default() {
|
|
|
2547
2547
|
}
|
|
2548
2548
|
schedule.on = on1;
|
|
2549
2549
|
});
|
|
2550
|
-
if (size === 0)
|
|
2550
|
+
if (size === 0) resolve2();
|
|
2551
2551
|
});
|
|
2552
2552
|
}
|
|
2553
2553
|
|
|
@@ -3508,13 +3508,13 @@ function areSetsEqual(a, b) {
|
|
|
3508
3508
|
return true;
|
|
3509
3509
|
}
|
|
3510
3510
|
function withResolvers() {
|
|
3511
|
-
let
|
|
3511
|
+
let resolve2;
|
|
3512
3512
|
let reject;
|
|
3513
3513
|
const promise = new Promise((res, rej) => {
|
|
3514
|
-
|
|
3514
|
+
resolve2 = res;
|
|
3515
3515
|
reject = rej;
|
|
3516
3516
|
});
|
|
3517
|
-
return { promise, resolve, reject };
|
|
3517
|
+
return { promise, resolve: resolve2, reject };
|
|
3518
3518
|
}
|
|
3519
3519
|
function mergeAriaLabelConfig(partial) {
|
|
3520
3520
|
return { ...defaultAriaLabelConfig, ...partial || {} };
|
|
@@ -5130,8 +5130,8 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
|
|
|
5130
5130
|
d3ZoomInstance.wheelDelta(wheelDelta);
|
|
5131
5131
|
async function setTransform(transform2, options) {
|
|
5132
5132
|
if (d3Selection) {
|
|
5133
|
-
return new Promise((
|
|
5134
|
-
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () =>
|
|
5133
|
+
return new Promise((resolve2) => {
|
|
5134
|
+
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), transform2);
|
|
5135
5135
|
});
|
|
5136
5136
|
}
|
|
5137
5137
|
return false;
|
|
@@ -5233,16 +5233,16 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
|
|
|
5233
5233
|
}
|
|
5234
5234
|
async function scaleTo(zoom, options) {
|
|
5235
5235
|
if (d3Selection) {
|
|
5236
|
-
return new Promise((
|
|
5237
|
-
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () =>
|
|
5236
|
+
return new Promise((resolve2) => {
|
|
5237
|
+
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), zoom);
|
|
5238
5238
|
});
|
|
5239
5239
|
}
|
|
5240
5240
|
return false;
|
|
5241
5241
|
}
|
|
5242
5242
|
async function scaleBy(factor, options) {
|
|
5243
5243
|
if (d3Selection) {
|
|
5244
|
-
return new Promise((
|
|
5245
|
-
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () =>
|
|
5244
|
+
return new Promise((resolve2) => {
|
|
5245
|
+
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), factor);
|
|
5246
5246
|
});
|
|
5247
5247
|
}
|
|
5248
5248
|
return false;
|
|
@@ -9030,11 +9030,11 @@ function NoteNodeInner(props) {
|
|
|
9030
9030
|
const config = data.config ?? {};
|
|
9031
9031
|
const api = useFlowEditorOptional();
|
|
9032
9032
|
const hasConfigText = typeof config.text === "string" && config.text.length > 0;
|
|
9033
|
-
const
|
|
9033
|
+
const text3 = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
|
|
9034
9034
|
const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
|
|
9035
9035
|
const skin = NOTE_COLORS[coerceColor(config.color)];
|
|
9036
9036
|
const [editing, setEditing] = ReactExports.useState(false);
|
|
9037
|
-
const [draft, setDraft] = ReactExports.useState(
|
|
9037
|
+
const [draft, setDraft] = ReactExports.useState(text3);
|
|
9038
9038
|
const areaRef = ReactExports.useRef(null);
|
|
9039
9039
|
ReactExports.useEffect(() => {
|
|
9040
9040
|
if (editing && areaRef.current) {
|
|
@@ -9045,7 +9045,7 @@ function NoteNodeInner(props) {
|
|
|
9045
9045
|
}, [editing]);
|
|
9046
9046
|
const commit = () => {
|
|
9047
9047
|
setEditing(false);
|
|
9048
|
-
if (!api || draft ===
|
|
9048
|
+
if (!api || draft === text3) return;
|
|
9049
9049
|
const current = api.nodes.find((n) => n.id === props.id);
|
|
9050
9050
|
if (!current) return;
|
|
9051
9051
|
api.updateNode({
|
|
@@ -9059,7 +9059,7 @@ function NoteNodeInner(props) {
|
|
|
9059
9059
|
className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
|
|
9060
9060
|
style: { background: skin.bg, borderColor: skin.border },
|
|
9061
9061
|
onDoubleClick: api ? () => {
|
|
9062
|
-
setDraft(
|
|
9062
|
+
setDraft(text3);
|
|
9063
9063
|
setEditing(true);
|
|
9064
9064
|
} : void 0,
|
|
9065
9065
|
children: [
|
|
@@ -9078,7 +9078,7 @@ function NoteNodeInner(props) {
|
|
|
9078
9078
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
|
|
9079
9079
|
}
|
|
9080
9080
|
}
|
|
9081
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children:
|
|
9081
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children: text3 || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
|
|
9082
9082
|
]
|
|
9083
9083
|
}
|
|
9084
9084
|
);
|
|
@@ -9243,6 +9243,376 @@ var subflowExecutor = async (ctx) => {
|
|
|
9243
9243
|
return { __port: "out", value: result.outputs };
|
|
9244
9244
|
};
|
|
9245
9245
|
|
|
9246
|
+
// src/expressions/expr.ts
|
|
9247
|
+
function wholeExpression(trimmed) {
|
|
9248
|
+
if (trimmed.length < 4) return null;
|
|
9249
|
+
if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
|
|
9250
|
+
return trimmed.slice(2, -2);
|
|
9251
|
+
}
|
|
9252
|
+
function interpolate(template, resolve2) {
|
|
9253
|
+
let out = "";
|
|
9254
|
+
let i = 0;
|
|
9255
|
+
for (; ; ) {
|
|
9256
|
+
const open = template.indexOf("{{", i);
|
|
9257
|
+
if (open === -1) return out + template.slice(i);
|
|
9258
|
+
const close = template.indexOf("}}", open + 2);
|
|
9259
|
+
if (close === -1) return out + template.slice(i);
|
|
9260
|
+
out += template.slice(i, open) + resolve2(template.slice(open + 2, close));
|
|
9261
|
+
i = close + 2;
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
|
|
9265
|
+
var UnresolvedPathError = class extends Error {
|
|
9266
|
+
constructor(path) {
|
|
9267
|
+
super(
|
|
9268
|
+
`Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
|
|
9269
|
+
);
|
|
9270
|
+
this.path = path;
|
|
9271
|
+
this.name = "UnresolvedPathError";
|
|
9272
|
+
}
|
|
9273
|
+
};
|
|
9274
|
+
function tryResolvePath(path, context) {
|
|
9275
|
+
const unresolved = { resolved: false, value: null };
|
|
9276
|
+
const trimmed = path.trim();
|
|
9277
|
+
if (trimmed === "") return unresolved;
|
|
9278
|
+
const segments = trimmed.split(".");
|
|
9279
|
+
let cursor;
|
|
9280
|
+
const head = segments[0];
|
|
9281
|
+
if (head === "$json" || head === "$input") {
|
|
9282
|
+
cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
|
|
9283
|
+
segments.shift();
|
|
9284
|
+
} else {
|
|
9285
|
+
cursor = context;
|
|
9286
|
+
}
|
|
9287
|
+
for (const segment of segments) {
|
|
9288
|
+
if (cursor === null || cursor === void 0) return unresolved;
|
|
9289
|
+
if (typeof cursor !== "object") return unresolved;
|
|
9290
|
+
const next = cursor[segment];
|
|
9291
|
+
if (next === void 0) return unresolved;
|
|
9292
|
+
cursor = next;
|
|
9293
|
+
}
|
|
9294
|
+
return { resolved: true, value: cursor === void 0 ? null : cursor };
|
|
9295
|
+
}
|
|
9296
|
+
function resolvePath(path, context) {
|
|
9297
|
+
return tryResolvePath(path, context).value;
|
|
9298
|
+
}
|
|
9299
|
+
function evaluateExpression(template, context, options = {}) {
|
|
9300
|
+
if (typeof template !== "string") return template;
|
|
9301
|
+
const policy = options.onUnresolved ?? "empty";
|
|
9302
|
+
const whole = wholeExpression(template.trim());
|
|
9303
|
+
if (whole !== null) {
|
|
9304
|
+
const r = tryResolvePath(whole, context);
|
|
9305
|
+
if (r.resolved) return r.value;
|
|
9306
|
+
if (policy === "throw") throw new UnresolvedPathError(whole);
|
|
9307
|
+
return policy === "keep" ? template : null;
|
|
9308
|
+
}
|
|
9309
|
+
return interpolate(template, (path) => {
|
|
9310
|
+
const r = tryResolvePath(path, context);
|
|
9311
|
+
if (r.resolved) return stringify(r.value);
|
|
9312
|
+
if (policy === "throw") throw new UnresolvedPathError(path);
|
|
9313
|
+
return policy === "keep" ? `{{${path}}}` : "";
|
|
9314
|
+
});
|
|
9315
|
+
}
|
|
9316
|
+
function truthy(value) {
|
|
9317
|
+
if (typeof value === "boolean") return value;
|
|
9318
|
+
if (value === null || value === void 0) return false;
|
|
9319
|
+
if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
|
|
9320
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
9321
|
+
if (typeof value === "number") return value !== 0;
|
|
9322
|
+
return Boolean(value);
|
|
9323
|
+
}
|
|
9324
|
+
function text(value) {
|
|
9325
|
+
return stringify(value);
|
|
9326
|
+
}
|
|
9327
|
+
function stringify(value) {
|
|
9328
|
+
if (typeof value === "string") return value;
|
|
9329
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
9330
|
+
if (value === null || value === void 0) return "";
|
|
9331
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
9332
|
+
try {
|
|
9333
|
+
return JSON.stringify(value) ?? "";
|
|
9334
|
+
} catch {
|
|
9335
|
+
return "";
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
function evaluateConfig(config, context, options = {}) {
|
|
9339
|
+
const out = {};
|
|
9340
|
+
for (const [key2, value] of Object.entries(config)) {
|
|
9341
|
+
out[key2] = Array.isArray(value) ? value.map((v) => evaluateExpression(v, context, options)) : value !== null && typeof value === "object" ? evaluateConfig(value, context, options) : evaluateExpression(value, context, options);
|
|
9342
|
+
}
|
|
9343
|
+
return out;
|
|
9344
|
+
}
|
|
9345
|
+
|
|
9346
|
+
// src/registry/logic.ts
|
|
9347
|
+
function configOf(node) {
|
|
9348
|
+
return node?.data?.config ?? {};
|
|
9349
|
+
}
|
|
9350
|
+
function contextOf(inputs) {
|
|
9351
|
+
return inputs;
|
|
9352
|
+
}
|
|
9353
|
+
function resolve(value, inputs) {
|
|
9354
|
+
if (typeof value !== "string") return value;
|
|
9355
|
+
return evaluateExpression(value, contextOf(inputs));
|
|
9356
|
+
}
|
|
9357
|
+
function conditionHolds(row, inputs) {
|
|
9358
|
+
const left = resolve(row.left, inputs);
|
|
9359
|
+
const right = resolve(row.right, inputs);
|
|
9360
|
+
const operator = typeof row.operator === "string" ? row.operator : "eq";
|
|
9361
|
+
const asText = (v) => text(v);
|
|
9362
|
+
const asNumber = (v) => {
|
|
9363
|
+
const n = typeof v === "number" ? v : Number(asText(v));
|
|
9364
|
+
return Number.isFinite(n) ? n : NaN;
|
|
9365
|
+
};
|
|
9366
|
+
const numeric = (fn) => {
|
|
9367
|
+
const a = asNumber(left);
|
|
9368
|
+
const b = asNumber(right);
|
|
9369
|
+
return Number.isFinite(a) && Number.isFinite(b) ? fn(a, b) : false;
|
|
9370
|
+
};
|
|
9371
|
+
const isEmpty = (v) => v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0 || typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0;
|
|
9372
|
+
switch (operator) {
|
|
9373
|
+
// Compared as TEXT, deliberately. `right` is a text field in the schema, so
|
|
9374
|
+
// `status == "2"` must match the number 2 an upstream node produced —
|
|
9375
|
+
// strict equality there would fail on a difference the author cannot see.
|
|
9376
|
+
case "eq":
|
|
9377
|
+
return asText(left) === asText(right);
|
|
9378
|
+
case "neq":
|
|
9379
|
+
return asText(left) !== asText(right);
|
|
9380
|
+
case "contains":
|
|
9381
|
+
return asText(left).includes(asText(right));
|
|
9382
|
+
case "not_contains":
|
|
9383
|
+
return !asText(left).includes(asText(right));
|
|
9384
|
+
case "gt":
|
|
9385
|
+
return numeric((a, b) => a > b);
|
|
9386
|
+
case "gte":
|
|
9387
|
+
return numeric((a, b) => a >= b);
|
|
9388
|
+
case "lt":
|
|
9389
|
+
return numeric((a, b) => a < b);
|
|
9390
|
+
case "lte":
|
|
9391
|
+
return numeric((a, b) => a <= b);
|
|
9392
|
+
case "truthy":
|
|
9393
|
+
return truthy(left);
|
|
9394
|
+
case "falsy":
|
|
9395
|
+
return !truthy(left);
|
|
9396
|
+
case "empty":
|
|
9397
|
+
return isEmpty(left);
|
|
9398
|
+
case "not_empty":
|
|
9399
|
+
return !isEmpty(left);
|
|
9400
|
+
default:
|
|
9401
|
+
return false;
|
|
9402
|
+
}
|
|
9403
|
+
}
|
|
9404
|
+
var branchExecutor = (ctx) => {
|
|
9405
|
+
const config = configOf(ctx.node);
|
|
9406
|
+
const inputs = ctx.inputs;
|
|
9407
|
+
let taken;
|
|
9408
|
+
const raw = config.condition;
|
|
9409
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
9410
|
+
taken = truthy(resolve(raw, inputs));
|
|
9411
|
+
} else {
|
|
9412
|
+
const rows = Array.isArray(config.conditions) ? config.conditions : [];
|
|
9413
|
+
if (rows.length === 0) {
|
|
9414
|
+
taken = false;
|
|
9415
|
+
} else {
|
|
9416
|
+
const results = rows.map((row) => conditionHolds(row, inputs));
|
|
9417
|
+
taken = config.match === "any" ? results.some(Boolean) : results.every(Boolean);
|
|
9418
|
+
}
|
|
9419
|
+
}
|
|
9420
|
+
return { __port: taken ? "true" : "false", value: ctx.inputs.in ?? ctx.inputs };
|
|
9421
|
+
};
|
|
9422
|
+
var transformExecutor = (ctx) => {
|
|
9423
|
+
const config = configOf(ctx.node);
|
|
9424
|
+
const inputs = ctx.inputs;
|
|
9425
|
+
const passthrough = ctx.inputs.in ?? ctx.inputs;
|
|
9426
|
+
if (config.mode === "expression") {
|
|
9427
|
+
const expression = config.expression;
|
|
9428
|
+
if (typeof expression !== "string" || expression.trim() === "") return passthrough;
|
|
9429
|
+
return resolve(expression, inputs);
|
|
9430
|
+
}
|
|
9431
|
+
const rows = Array.isArray(config.fields) ? config.fields : [];
|
|
9432
|
+
if (rows.length === 0) return passthrough;
|
|
9433
|
+
const out = {};
|
|
9434
|
+
let wrote = false;
|
|
9435
|
+
for (const row of rows) {
|
|
9436
|
+
const key2 = typeof row.key === "string" ? row.key.trim() : "";
|
|
9437
|
+
if (key2 === "") continue;
|
|
9438
|
+
out[key2] = resolve(row.value, inputs);
|
|
9439
|
+
wrote = true;
|
|
9440
|
+
}
|
|
9441
|
+
return wrote ? out : passthrough;
|
|
9442
|
+
};
|
|
9443
|
+
var mergeExecutor = (ctx) => {
|
|
9444
|
+
const entries = Object.entries(ctx.inputs);
|
|
9445
|
+
if (configOf(ctx.node).mode === "concat") {
|
|
9446
|
+
const out = [];
|
|
9447
|
+
for (const [, value] of entries) {
|
|
9448
|
+
if (value === null || value === void 0) continue;
|
|
9449
|
+
if (Array.isArray(value)) out.push(...value);
|
|
9450
|
+
else out.push(value);
|
|
9451
|
+
}
|
|
9452
|
+
return out;
|
|
9453
|
+
}
|
|
9454
|
+
const merged = {};
|
|
9455
|
+
for (const [port, value] of entries) {
|
|
9456
|
+
if (value === null || value === void 0) continue;
|
|
9457
|
+
if (typeof value === "object" && !Array.isArray(value)) Object.assign(merged, value);
|
|
9458
|
+
else merged[port] = value;
|
|
9459
|
+
}
|
|
9460
|
+
return merged;
|
|
9461
|
+
};
|
|
9462
|
+
var forEachExecutor = (ctx) => {
|
|
9463
|
+
const source = resolve(configOf(ctx.node).source, ctx.inputs);
|
|
9464
|
+
let items;
|
|
9465
|
+
if (Array.isArray(source)) items = source;
|
|
9466
|
+
else if (source === null || source === void 0) items = [];
|
|
9467
|
+
else if (typeof source === "object") items = Object.values(source);
|
|
9468
|
+
else items = [source];
|
|
9469
|
+
return { items, count: items.length };
|
|
9470
|
+
};
|
|
9471
|
+
|
|
9472
|
+
// src/registry/terminal-nodes.ts
|
|
9473
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9474
|
+
var ENTER = "\r";
|
|
9475
|
+
function configOf2(node) {
|
|
9476
|
+
return node?.data?.config ?? {};
|
|
9477
|
+
}
|
|
9478
|
+
function text2(config, key2) {
|
|
9479
|
+
const value = config[key2];
|
|
9480
|
+
return typeof value === "string" ? value : "";
|
|
9481
|
+
}
|
|
9482
|
+
function millis(config, key2, fallback) {
|
|
9483
|
+
const value = Number(config[key2]);
|
|
9484
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
9485
|
+
}
|
|
9486
|
+
function exitMarker(nonce) {
|
|
9487
|
+
const token = `__fancy_flow_exit_${nonce}__`;
|
|
9488
|
+
return {
|
|
9489
|
+
token,
|
|
9490
|
+
// The expanded status is required to be digits. The shell ECHOES the typed
|
|
9491
|
+
// command back first, and that echo contains the marker with `$?`
|
|
9492
|
+
// unexpanded — so requiring digits is what stops the node matching its own
|
|
9493
|
+
// command line and reporting success before anything has run.
|
|
9494
|
+
pattern: new RegExp(`${token}:(\\d+)`)
|
|
9495
|
+
};
|
|
9496
|
+
}
|
|
9497
|
+
var counter = 0;
|
|
9498
|
+
function newNonce() {
|
|
9499
|
+
counter += 1;
|
|
9500
|
+
return `${Date.now().toString(36)}${counter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
9501
|
+
}
|
|
9502
|
+
function withoutMarker(output, token) {
|
|
9503
|
+
return output.split("\n").filter((line) => !line.includes(token)).join("\n").trim();
|
|
9504
|
+
}
|
|
9505
|
+
function fail(ctx, message) {
|
|
9506
|
+
ctx.abort(message);
|
|
9507
|
+
throw new Error(message);
|
|
9508
|
+
}
|
|
9509
|
+
function requireTerminal(ctx) {
|
|
9510
|
+
if (!ctx.terminal) {
|
|
9511
|
+
fail(
|
|
9512
|
+
ctx,
|
|
9513
|
+
`"${String(ctx.node.data?.label ?? ctx.node.id)}" is a terminal node but is not inside a terminal lane. Drag it into one \u2014 a terminal node outside a lane has no session to talk to, and opening a private shell for it would defeat the point of the lane.`
|
|
9514
|
+
);
|
|
9515
|
+
}
|
|
9516
|
+
return ctx.terminal;
|
|
9517
|
+
}
|
|
9518
|
+
function compilePattern(ctx, raw, config) {
|
|
9519
|
+
const flags = typeof config.flags === "string" ? config.flags.replace(/[gy]/g, "") : "";
|
|
9520
|
+
try {
|
|
9521
|
+
return config.mode === "regex" ? new RegExp(raw, flags) : new RegExp(raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
|
|
9522
|
+
} catch (e) {
|
|
9523
|
+
return fail(ctx, `terminal_await has an invalid regex: ${e instanceof Error ? e.message : String(e)}`);
|
|
9524
|
+
}
|
|
9525
|
+
}
|
|
9526
|
+
var terminalRunExecutor = async (ctx) => {
|
|
9527
|
+
const config = configOf2(ctx.node);
|
|
9528
|
+
const command = text2(config, "command").trim();
|
|
9529
|
+
if (!command) fail(ctx, "terminal_run has no command configured");
|
|
9530
|
+
const terminal = requireTerminal(ctx);
|
|
9531
|
+
const session = await terminal.session();
|
|
9532
|
+
const transcript = await terminal.transcript();
|
|
9533
|
+
const { token, pattern } = exitMarker(newNonce());
|
|
9534
|
+
const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
|
|
9535
|
+
transcript.clear();
|
|
9536
|
+
await session.write(`${command}; printf '${token}:%s\\n' "$?"${ENTER}`);
|
|
9537
|
+
const result = await transcript.waitFor(pattern, { timeoutMs, exited: session.exited });
|
|
9538
|
+
if (result.status === "exited") {
|
|
9539
|
+
fail(
|
|
9540
|
+
ctx,
|
|
9541
|
+
`The terminal exited (code ${result.exitCode}${result.signal ? `, signal ${result.signal}` : ""}) while running "${command}". The lane's session is gone, so every later node in this lane would fail too.`
|
|
9542
|
+
);
|
|
9543
|
+
}
|
|
9544
|
+
if (result.status === "timeout") {
|
|
9545
|
+
fail(
|
|
9546
|
+
ctx,
|
|
9547
|
+
`"${command}" did not finish within ${timeoutMs}ms. Raise the node's timeout if it is genuinely slow \u2014 but a command that waits for input never finishes at all, and needs terminal_send + terminal_await.`
|
|
9548
|
+
);
|
|
9549
|
+
}
|
|
9550
|
+
const exitCode = Number(result.match[1]);
|
|
9551
|
+
const output = withoutMarker(result.text, token);
|
|
9552
|
+
const failOnNonZero = config.failOnNonZero !== false;
|
|
9553
|
+
if (failOnNonZero && exitCode !== 0) {
|
|
9554
|
+
ctx.abort(`"${command}" exited ${exitCode}.
|
|
9555
|
+
${output}`);
|
|
9556
|
+
}
|
|
9557
|
+
return { output, exitCode, command };
|
|
9558
|
+
};
|
|
9559
|
+
var terminalSendExecutor = async (ctx) => {
|
|
9560
|
+
const config = configOf2(ctx.node);
|
|
9561
|
+
const terminal = requireTerminal(ctx);
|
|
9562
|
+
const session = await terminal.session();
|
|
9563
|
+
const body = text2(config, "text");
|
|
9564
|
+
const submit = config.submit !== false;
|
|
9565
|
+
if (config.clearFirst === true) {
|
|
9566
|
+
(await terminal.transcript()).clear();
|
|
9567
|
+
}
|
|
9568
|
+
await session.write(submit ? `${body}${ENTER}` : body);
|
|
9569
|
+
return { sent: body, submitted: submit };
|
|
9570
|
+
};
|
|
9571
|
+
var terminalAwaitExecutor = async (ctx) => {
|
|
9572
|
+
const config = configOf2(ctx.node);
|
|
9573
|
+
const terminal = requireTerminal(ctx);
|
|
9574
|
+
const session = await terminal.session();
|
|
9575
|
+
const transcript = await terminal.transcript();
|
|
9576
|
+
const raw = text2(config, "pattern").trim();
|
|
9577
|
+
if (!raw) fail(ctx, "terminal_await has no pattern configured \u2014 there is nothing for it to wait for");
|
|
9578
|
+
const pattern = compilePattern(ctx, raw, config);
|
|
9579
|
+
const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
|
|
9580
|
+
const result = await transcript.waitFor(pattern, {
|
|
9581
|
+
timeoutMs,
|
|
9582
|
+
exited: session.exited
|
|
9583
|
+
});
|
|
9584
|
+
if (result.status === "exited") {
|
|
9585
|
+
fail(
|
|
9586
|
+
ctx,
|
|
9587
|
+
`The terminal exited (code ${result.exitCode}${result.signal ? `, signal ${result.signal}` : ""}) while waiting for ${JSON.stringify(raw)}. It never appeared, and the lane's session is gone.`
|
|
9588
|
+
);
|
|
9589
|
+
}
|
|
9590
|
+
if (result.status === "timeout") {
|
|
9591
|
+
if (config.onTimeout === "continue") {
|
|
9592
|
+
ctx.emit({
|
|
9593
|
+
type: "log",
|
|
9594
|
+
nodeId: ctx.node.id,
|
|
9595
|
+
level: "warn",
|
|
9596
|
+
message: `terminal_await: ${JSON.stringify(raw)} did not appear within ${timeoutMs}ms; continuing as configured.`
|
|
9597
|
+
});
|
|
9598
|
+
return { matched: false, output: result.text, groups: [] };
|
|
9599
|
+
}
|
|
9600
|
+
fail(
|
|
9601
|
+
ctx,
|
|
9602
|
+
`${JSON.stringify(raw)} did not appear within ${timeoutMs}ms. Last output:
|
|
9603
|
+
${result.text.slice(-2e3)}`
|
|
9604
|
+
);
|
|
9605
|
+
}
|
|
9606
|
+
return {
|
|
9607
|
+
matched: true,
|
|
9608
|
+
output: result.text,
|
|
9609
|
+
matchedText: result.match[0],
|
|
9610
|
+
// Capture groups are the reason to use regex mode at all — a prompt that
|
|
9611
|
+
// reports a session id or a file path is only useful if the value comes out.
|
|
9612
|
+
groups: result.match.slice(1)
|
|
9613
|
+
};
|
|
9614
|
+
};
|
|
9615
|
+
|
|
9246
9616
|
// src/registry/builtin.ts
|
|
9247
9617
|
var HUMAN_FIELD_OUTPUT_TYPE = {
|
|
9248
9618
|
text: "string",
|
|
@@ -9532,6 +9902,9 @@ var KINDS = [
|
|
|
9532
9902
|
// returns its input on the chosen port
|
|
9533
9903
|
emits: "input",
|
|
9534
9904
|
aliases: ["branch", "@fancy/branch"],
|
|
9905
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
9906
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
9907
|
+
executor: branchExecutor,
|
|
9535
9908
|
category: "logic",
|
|
9536
9909
|
label: "Branch",
|
|
9537
9910
|
description: "Multi-way branch on a condition or value.",
|
|
@@ -9629,6 +10002,9 @@ var KINDS = [
|
|
|
9629
10002
|
{ path: "count", type: "number" }
|
|
9630
10003
|
],
|
|
9631
10004
|
aliases: ["for_each", "@fancy/for_each"],
|
|
10005
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
10006
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
10007
|
+
executor: forEachExecutor,
|
|
9632
10008
|
category: "logic",
|
|
9633
10009
|
label: "For Each",
|
|
9634
10010
|
description: "Iterate over a list, emitting each item on `item`.",
|
|
@@ -9649,6 +10025,9 @@ var KINDS = [
|
|
|
9649
10025
|
// reference.
|
|
9650
10026
|
emits: (config) => (config?.mode ?? "merge") === "concat" ? null : "inputs-merged",
|
|
9651
10027
|
aliases: ["merge", "@fancy/merge"],
|
|
10028
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
10029
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
10030
|
+
executor: mergeExecutor,
|
|
9652
10031
|
category: "logic",
|
|
9653
10032
|
label: "Merge",
|
|
9654
10033
|
description: "Combine multiple inputs into one object or array.",
|
|
@@ -9694,6 +10073,9 @@ var KINDS = [
|
|
|
9694
10073
|
// else the shape that expression names. So the RELATION is config-dependent.
|
|
9695
10074
|
emits: (config) => (config?.expression ?? "") === "" ? "input" : "expression:expression",
|
|
9696
10075
|
aliases: ["transform", "@fancy/transform"],
|
|
10076
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
10077
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
10078
|
+
executor: transformExecutor,
|
|
9697
10079
|
category: "logic",
|
|
9698
10080
|
label: "Transform",
|
|
9699
10081
|
description: "Reshape data with an expression.",
|
|
@@ -10152,6 +10534,179 @@ var KINDS = [
|
|
|
10152
10534
|
resizable: { minWidth: 160, minHeight: 72 },
|
|
10153
10535
|
component: LaneNode
|
|
10154
10536
|
},
|
|
10537
|
+
{
|
|
10538
|
+
// Terminal lane — a lane that OWNS a terminal for the length of the run.
|
|
10539
|
+
//
|
|
10540
|
+
// Visually a swimlane, and `layout` for the same reason the plain lane is:
|
|
10541
|
+
// a lane is not a step, so it never executes and never appears in topo
|
|
10542
|
+
// order. What is new is that it DECLARES a resource. The runtime opens its
|
|
10543
|
+
// terminal lazily, when the first terminal node inside it runs, and closes
|
|
10544
|
+
// it in the run's `finally` — so a graph that never reaches a terminal node
|
|
10545
|
+
// never spawns a process, and one that does gets exactly one session no
|
|
10546
|
+
// matter how many nodes use it.
|
|
10547
|
+
//
|
|
10548
|
+
// Membership is `parentId`, which already persists into the WorkflowSchema.
|
|
10549
|
+
// That is what lets a headless runtime resolve the same grouping the canvas
|
|
10550
|
+
// shows, without a second association to keep in sync.
|
|
10551
|
+
name: "@particle-academy/terminal_lane",
|
|
10552
|
+
aliases: ["terminal_lane", "@fancy/terminal_lane"],
|
|
10553
|
+
category: "layout",
|
|
10554
|
+
label: "Terminal lane",
|
|
10555
|
+
description: "A lane that owns one terminal. Opens at the first terminal node, closes when the run ends.",
|
|
10556
|
+
icon: "\u25A3",
|
|
10557
|
+
inputs: [],
|
|
10558
|
+
outputs: [],
|
|
10559
|
+
configSchema: [
|
|
10560
|
+
{ type: "text", key: "title", label: "Title", default: "Terminal" },
|
|
10561
|
+
{
|
|
10562
|
+
type: "text",
|
|
10563
|
+
key: "command",
|
|
10564
|
+
label: "Command",
|
|
10565
|
+
placeholder: "Leave empty for the default shell"
|
|
10566
|
+
},
|
|
10567
|
+
{ type: "text", key: "cwd", label: "Working directory", placeholder: "Host default" },
|
|
10568
|
+
{ type: "keyvalue", key: "env", label: "Environment" },
|
|
10569
|
+
{
|
|
10570
|
+
type: "select",
|
|
10571
|
+
key: "orientation",
|
|
10572
|
+
label: "Orientation",
|
|
10573
|
+
default: "horizontal",
|
|
10574
|
+
options: [
|
|
10575
|
+
{ value: "horizontal", label: "Row" },
|
|
10576
|
+
{ value: "vertical", label: "Column" }
|
|
10577
|
+
]
|
|
10578
|
+
}
|
|
10579
|
+
],
|
|
10580
|
+
defaultConfig: { title: "Terminal", orientation: "horizontal" },
|
|
10581
|
+
resizable: { minWidth: 220, minHeight: 120 },
|
|
10582
|
+
component: LaneNode
|
|
10583
|
+
},
|
|
10584
|
+
{
|
|
10585
|
+
// Terminal nodes. All three live inside a terminal lane and talk to the
|
|
10586
|
+
// session that lane owns; outside one they abort by name rather than
|
|
10587
|
+
// opening a shell of their own.
|
|
10588
|
+
name: "@particle-academy/terminal_run",
|
|
10589
|
+
aliases: ["terminal_run", "@fancy/terminal_run"],
|
|
10590
|
+
category: "io",
|
|
10591
|
+
label: "Run in terminal",
|
|
10592
|
+
description: "Run a shell command in the lane's terminal and wait for its exit code.",
|
|
10593
|
+
icon: "$",
|
|
10594
|
+
inputs: [{ id: "in" }],
|
|
10595
|
+
outputs: [{ id: "out", label: "output" }],
|
|
10596
|
+
executor: terminalRunExecutor,
|
|
10597
|
+
configSchema: [
|
|
10598
|
+
{
|
|
10599
|
+
type: "textarea",
|
|
10600
|
+
key: "command",
|
|
10601
|
+
label: "Command",
|
|
10602
|
+
rows: 3,
|
|
10603
|
+
required: true,
|
|
10604
|
+
placeholder: "npm test",
|
|
10605
|
+
description: "Runs in the lane's shell, so `cd` and exported variables persist between nodes. SHELL ONLY \u2014 a TUI never returns to a prompt, so use Send + Await for one."
|
|
10606
|
+
},
|
|
10607
|
+
{
|
|
10608
|
+
type: "number",
|
|
10609
|
+
key: "timeoutMs",
|
|
10610
|
+
label: "Timeout (ms)",
|
|
10611
|
+
default: 12e4,
|
|
10612
|
+
description: "How long to wait for the command to finish before failing the run."
|
|
10613
|
+
},
|
|
10614
|
+
{
|
|
10615
|
+
type: "switch",
|
|
10616
|
+
key: "failOnNonZero",
|
|
10617
|
+
label: "Fail the run on a non-zero exit",
|
|
10618
|
+
default: true,
|
|
10619
|
+
description: "On by default. Turning it off means a failed command lets the run report success \u2014 read the exit code on the output port and branch on it instead."
|
|
10620
|
+
}
|
|
10621
|
+
],
|
|
10622
|
+
defaultConfig: { command: "", timeoutMs: 12e4, failOnNonZero: true }
|
|
10623
|
+
},
|
|
10624
|
+
{
|
|
10625
|
+
name: "@particle-academy/terminal_send",
|
|
10626
|
+
aliases: ["terminal_send", "@fancy/terminal_send"],
|
|
10627
|
+
category: "io",
|
|
10628
|
+
label: "Send to terminal",
|
|
10629
|
+
description: "Type text at whatever is running in the lane's terminal, without waiting.",
|
|
10630
|
+
icon: "\u2328",
|
|
10631
|
+
inputs: [{ id: "in" }],
|
|
10632
|
+
outputs: [{ id: "out", label: "sent" }],
|
|
10633
|
+
executor: terminalSendExecutor,
|
|
10634
|
+
configSchema: [
|
|
10635
|
+
{
|
|
10636
|
+
type: "textarea",
|
|
10637
|
+
key: "text",
|
|
10638
|
+
label: "Text",
|
|
10639
|
+
rows: 4,
|
|
10640
|
+
placeholder: "Summarise the failing test and propose a fix.",
|
|
10641
|
+
description: "What to type. This is how a graph prompts an agent TUI such as Claude Code or Codex."
|
|
10642
|
+
},
|
|
10643
|
+
{
|
|
10644
|
+
type: "switch",
|
|
10645
|
+
key: "submit",
|
|
10646
|
+
label: "Press Enter",
|
|
10647
|
+
default: true,
|
|
10648
|
+
description: "Off leaves the text on the input line \u2014 useful for building one up across several nodes."
|
|
10649
|
+
},
|
|
10650
|
+
{
|
|
10651
|
+
type: "switch",
|
|
10652
|
+
key: "clearFirst",
|
|
10653
|
+
label: "Forget earlier output first",
|
|
10654
|
+
default: false,
|
|
10655
|
+
description: "Discards anything the terminal said before this send, so a following Await cannot match a prompt left over from the previous exchange."
|
|
10656
|
+
}
|
|
10657
|
+
],
|
|
10658
|
+
defaultConfig: { text: "", submit: true, clearFirst: false }
|
|
10659
|
+
},
|
|
10660
|
+
{
|
|
10661
|
+
name: "@particle-academy/terminal_await",
|
|
10662
|
+
aliases: ["terminal_await", "@fancy/terminal_await"],
|
|
10663
|
+
category: "io",
|
|
10664
|
+
label: "Await terminal output",
|
|
10665
|
+
description: "Wait until the lane's terminal prints something that matches.",
|
|
10666
|
+
icon: "\u23F1",
|
|
10667
|
+
inputs: [{ id: "in" }],
|
|
10668
|
+
outputs: [{ id: "out", label: "output" }],
|
|
10669
|
+
executor: terminalAwaitExecutor,
|
|
10670
|
+
configSchema: [
|
|
10671
|
+
{
|
|
10672
|
+
type: "text",
|
|
10673
|
+
key: "pattern",
|
|
10674
|
+
label: "Wait for",
|
|
10675
|
+
required: true,
|
|
10676
|
+
placeholder: "esc to interrupt",
|
|
10677
|
+
description: "Matched against the output with colour codes already stripped, so match what you SEE."
|
|
10678
|
+
},
|
|
10679
|
+
{
|
|
10680
|
+
type: "select",
|
|
10681
|
+
key: "mode",
|
|
10682
|
+
label: "Match as",
|
|
10683
|
+
default: "text",
|
|
10684
|
+
options: [
|
|
10685
|
+
{ value: "text", label: "Plain text" },
|
|
10686
|
+
{ value: "regex", label: "Regular expression" }
|
|
10687
|
+
],
|
|
10688
|
+
description: "Regex mode also returns capture groups, which is how a value gets out of a prompt."
|
|
10689
|
+
},
|
|
10690
|
+
{
|
|
10691
|
+
type: "number",
|
|
10692
|
+
key: "timeoutMs",
|
|
10693
|
+
label: "Timeout (ms)",
|
|
10694
|
+
default: 12e4
|
|
10695
|
+
},
|
|
10696
|
+
{
|
|
10697
|
+
type: "select",
|
|
10698
|
+
key: "onTimeout",
|
|
10699
|
+
label: "If it never appears",
|
|
10700
|
+
default: "fail",
|
|
10701
|
+
options: [
|
|
10702
|
+
{ value: "fail", label: "Fail the run" },
|
|
10703
|
+
{ value: "continue", label: "Continue with matched: false" }
|
|
10704
|
+
],
|
|
10705
|
+
description: "Failing is the default. Continuing lets the next node type at a process that never became ready while the run still reports success, so it has to be asked for."
|
|
10706
|
+
}
|
|
10707
|
+
],
|
|
10708
|
+
defaultConfig: { pattern: "", mode: "text", timeoutMs: 12e4, onTimeout: "fail" }
|
|
10709
|
+
},
|
|
10155
10710
|
{
|
|
10156
10711
|
// Note — a sticky-note annotation. Portless + visual-only: the runtime skips
|
|
10157
10712
|
// the `annotation` category, so a note's text NEVER reaches a runner — it
|
|
@@ -10489,6 +11044,239 @@ var RunIdentity = class _RunIdentity {
|
|
|
10489
11044
|
}
|
|
10490
11045
|
};
|
|
10491
11046
|
|
|
11047
|
+
// src/runtime/terminal-transcript.ts
|
|
11048
|
+
var ESC = String.fromCharCode(27);
|
|
11049
|
+
var CSI = String.fromCharCode(155);
|
|
11050
|
+
var BEL = String.fromCharCode(7);
|
|
11051
|
+
var ANSI = new RegExp(
|
|
11052
|
+
`[${ESC}${CSI}](?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|[@-Z\\\\-_])`,
|
|
11053
|
+
"g"
|
|
11054
|
+
);
|
|
11055
|
+
var MAX_PENDING_ESCAPE = 1024;
|
|
11056
|
+
var MAX_BUFFER = 1e6;
|
|
11057
|
+
var TerminalTranscript = class {
|
|
11058
|
+
constructor() {
|
|
11059
|
+
/** Stripped, complete text not yet consumed by a wait. */
|
|
11060
|
+
this.text = "";
|
|
11061
|
+
/** Raw bytes held back because they may be the start of an escape sequence. */
|
|
11062
|
+
this.tail = "";
|
|
11063
|
+
this.waiters = /* @__PURE__ */ new Set();
|
|
11064
|
+
}
|
|
11065
|
+
/** Feed raw terminal output in. Safe to call with any chunking. */
|
|
11066
|
+
append(chunk) {
|
|
11067
|
+
this.tail += chunk;
|
|
11068
|
+
const safe = this.safeLength();
|
|
11069
|
+
if (safe > 0) {
|
|
11070
|
+
this.text += this.tail.slice(0, safe).replace(ANSI, "");
|
|
11071
|
+
this.tail = this.tail.slice(safe);
|
|
11072
|
+
}
|
|
11073
|
+
if (this.text.length > MAX_BUFFER) {
|
|
11074
|
+
this.text = this.text.slice(this.text.length - MAX_BUFFER);
|
|
11075
|
+
}
|
|
11076
|
+
for (const wake2 of [...this.waiters]) wake2();
|
|
11077
|
+
}
|
|
11078
|
+
/**
|
|
11079
|
+
* How much of `tail` can be stripped now.
|
|
11080
|
+
*
|
|
11081
|
+
* Everything, unless the final `ESC` has not yet been terminated — in which
|
|
11082
|
+
* case processing stops there and resumes when the rest arrives.
|
|
11083
|
+
*/
|
|
11084
|
+
safeLength() {
|
|
11085
|
+
const lastEsc = Math.max(this.tail.lastIndexOf(ESC), this.tail.lastIndexOf(CSI));
|
|
11086
|
+
if (lastEsc === -1) return this.tail.length;
|
|
11087
|
+
ANSI.lastIndex = lastEsc;
|
|
11088
|
+
const match = ANSI.exec(this.tail);
|
|
11089
|
+
ANSI.lastIndex = 0;
|
|
11090
|
+
if (match && match.index === lastEsc) return this.tail.length;
|
|
11091
|
+
if (this.tail.length - lastEsc > MAX_PENDING_ESCAPE) return this.tail.length;
|
|
11092
|
+
return lastEsc;
|
|
11093
|
+
}
|
|
11094
|
+
/** Unconsumed output, escape sequences removed. */
|
|
11095
|
+
peek() {
|
|
11096
|
+
return this.text;
|
|
11097
|
+
}
|
|
11098
|
+
/** Drop everything currently buffered — used before typing a new command. */
|
|
11099
|
+
clear() {
|
|
11100
|
+
this.text = "";
|
|
11101
|
+
this.tail = "";
|
|
11102
|
+
}
|
|
11103
|
+
/**
|
|
11104
|
+
* Wait until `pattern` matches the unconsumed text.
|
|
11105
|
+
*
|
|
11106
|
+
* Checks what has ALREADY arrived before subscribing, because the common case
|
|
11107
|
+
* is that the process answered while the previous node was still finishing.
|
|
11108
|
+
*
|
|
11109
|
+
* `exited` is raced deliberately. Without it, a shell that dies reports as
|
|
11110
|
+
* "timed out waiting for X" — sending whoever reads it to lengthen a timeout
|
|
11111
|
+
* for a process that is not running. Naming the exit is the difference
|
|
11112
|
+
* between a diagnosis and a wrong lead.
|
|
11113
|
+
*/
|
|
11114
|
+
waitFor(pattern, options) {
|
|
11115
|
+
return new Promise((resolve2) => {
|
|
11116
|
+
let settled = false;
|
|
11117
|
+
let timer2;
|
|
11118
|
+
const finish = (result) => {
|
|
11119
|
+
if (settled) return;
|
|
11120
|
+
settled = true;
|
|
11121
|
+
this.waiters.delete(check);
|
|
11122
|
+
if (timer2) clearTimeout(timer2);
|
|
11123
|
+
resolve2(result);
|
|
11124
|
+
};
|
|
11125
|
+
const check = () => {
|
|
11126
|
+
pattern.lastIndex = 0;
|
|
11127
|
+
const match = pattern.exec(this.text);
|
|
11128
|
+
if (!match) return;
|
|
11129
|
+
const through = match.index + (match[0].length || 1);
|
|
11130
|
+
const text3 = this.text.slice(0, through);
|
|
11131
|
+
this.text = this.text.slice(through);
|
|
11132
|
+
finish({ status: "matched", text: text3, match });
|
|
11133
|
+
};
|
|
11134
|
+
this.waiters.add(check);
|
|
11135
|
+
check();
|
|
11136
|
+
if (settled) return;
|
|
11137
|
+
if (options.timeoutMs > 0) {
|
|
11138
|
+
timer2 = setTimeout(() => {
|
|
11139
|
+
const text3 = this.text;
|
|
11140
|
+
this.text = "";
|
|
11141
|
+
finish({ status: "timeout", text: text3 });
|
|
11142
|
+
}, options.timeoutMs);
|
|
11143
|
+
timer2.unref?.();
|
|
11144
|
+
}
|
|
11145
|
+
options.exited?.then(
|
|
11146
|
+
(exit) => {
|
|
11147
|
+
if (settled) return;
|
|
11148
|
+
const text3 = this.text;
|
|
11149
|
+
this.text = "";
|
|
11150
|
+
finish({ status: "exited", text: text3, exitCode: exit.exitCode, signal: exit.signal });
|
|
11151
|
+
},
|
|
11152
|
+
() => {
|
|
11153
|
+
}
|
|
11154
|
+
);
|
|
11155
|
+
});
|
|
11156
|
+
}
|
|
11157
|
+
};
|
|
11158
|
+
|
|
11159
|
+
// src/runtime/terminal-sessions.ts
|
|
11160
|
+
var TerminalSessions = class {
|
|
11161
|
+
constructor(graph) {
|
|
11162
|
+
this.graph = graph;
|
|
11163
|
+
this.open = /* @__PURE__ */ new Map();
|
|
11164
|
+
this.laneOf = /* @__PURE__ */ new Map();
|
|
11165
|
+
this.transcripts = /* @__PURE__ */ new Map();
|
|
11166
|
+
this.listening = /* @__PURE__ */ new Map();
|
|
11167
|
+
}
|
|
11168
|
+
/**
|
|
11169
|
+
* The terminal lane a node belongs to, or null.
|
|
11170
|
+
*
|
|
11171
|
+
* Walks up `parentId` rather than checking only the immediate parent, and
|
|
11172
|
+
* caches per node id — a graph is walked once per run, and the answer cannot
|
|
11173
|
+
* change while it runs.
|
|
11174
|
+
*/
|
|
11175
|
+
laneFor(nodeId, isTerminalLane) {
|
|
11176
|
+
const cached = this.laneOf.get(nodeId);
|
|
11177
|
+
if (cached !== void 0) return cached;
|
|
11178
|
+
const byId = new Map(this.graph.nodes.map((n) => [n.id, n]));
|
|
11179
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11180
|
+
let current = byId.get(nodeId);
|
|
11181
|
+
let answer = null;
|
|
11182
|
+
while (current) {
|
|
11183
|
+
if (seen.has(current.id)) break;
|
|
11184
|
+
seen.add(current.id);
|
|
11185
|
+
if (current.id !== nodeId && isTerminalLane(current)) {
|
|
11186
|
+
answer = current.id;
|
|
11187
|
+
break;
|
|
11188
|
+
}
|
|
11189
|
+
const parentId = current.parentId;
|
|
11190
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
11191
|
+
}
|
|
11192
|
+
this.laneOf.set(nodeId, answer);
|
|
11193
|
+
return answer;
|
|
11194
|
+
}
|
|
11195
|
+
/**
|
|
11196
|
+
* The session for a lane, opening it on first use.
|
|
11197
|
+
*
|
|
11198
|
+
* The PROMISE is cached, not the resolved session. Two nodes cannot run
|
|
11199
|
+
* concurrently in the current engine, but a caller that awaits a
|
|
11200
|
+
* half-finished open would otherwise start a second process — and two shells
|
|
11201
|
+
* where a graph says one is the failure this class exists to prevent, arriving
|
|
11202
|
+
* only under concurrency and therefore only sometimes.
|
|
11203
|
+
*/
|
|
11204
|
+
session(laneId, spec) {
|
|
11205
|
+
const existing = this.open.get(laneId);
|
|
11206
|
+
if (existing) return existing;
|
|
11207
|
+
{
|
|
11208
|
+
const failed = Promise.reject(
|
|
11209
|
+
new Error(
|
|
11210
|
+
"No terminal host is registered. A terminal lane needs one \u2014 call registerTerminalHost() from the desktop app, or install @particle-academy/fancy-flow/terminal/fancy-term-host."
|
|
11211
|
+
)
|
|
11212
|
+
);
|
|
11213
|
+
this.open.set(laneId, failed);
|
|
11214
|
+
failed.catch(() => {
|
|
11215
|
+
});
|
|
11216
|
+
return failed;
|
|
11217
|
+
}
|
|
11218
|
+
}
|
|
11219
|
+
/**
|
|
11220
|
+
* The accumulated output for a lane.
|
|
11221
|
+
*
|
|
11222
|
+
* Created on demand and kept for the run, so it exists before the session
|
|
11223
|
+
* resolves and survives every node that reads it. Consuming is the reader's
|
|
11224
|
+
* job — see `TerminalTranscript.waitFor`.
|
|
11225
|
+
*/
|
|
11226
|
+
transcriptFor(laneId) {
|
|
11227
|
+
const existing = this.transcripts.get(laneId);
|
|
11228
|
+
if (existing) return existing;
|
|
11229
|
+
const created = new TerminalTranscript();
|
|
11230
|
+
this.transcripts.set(laneId, created);
|
|
11231
|
+
return created;
|
|
11232
|
+
}
|
|
11233
|
+
/** True once a lane has a session — used to avoid opening one during teardown. */
|
|
11234
|
+
isOpen(laneId) {
|
|
11235
|
+
return this.open.has(laneId);
|
|
11236
|
+
}
|
|
11237
|
+
/**
|
|
11238
|
+
* Close every session this run opened.
|
|
11239
|
+
*
|
|
11240
|
+
* Every close is attempted even if one throws: a host that fails to close one
|
|
11241
|
+
* PTY must not strand the others, which would leave processes alive after the
|
|
11242
|
+
* run reported that it had finished. Errors are returned rather than thrown,
|
|
11243
|
+
* because teardown runs in a `finally` and throwing there would replace the
|
|
11244
|
+
* run's real error with a cleanup error.
|
|
11245
|
+
*/
|
|
11246
|
+
async closeAll() {
|
|
11247
|
+
const errors = [];
|
|
11248
|
+
for (const [laneId, pending] of this.open) {
|
|
11249
|
+
try {
|
|
11250
|
+
this.listening.get(laneId)?.();
|
|
11251
|
+
this.listening.delete(laneId);
|
|
11252
|
+
const session = await pending;
|
|
11253
|
+
await session.close();
|
|
11254
|
+
} catch (e) {
|
|
11255
|
+
errors.push(e instanceof Error ? e : new Error(`${laneId}: ${String(e)}`));
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11258
|
+
this.open.clear();
|
|
11259
|
+
this.listening.clear();
|
|
11260
|
+
this.transcripts.clear();
|
|
11261
|
+
return errors;
|
|
11262
|
+
}
|
|
11263
|
+
};
|
|
11264
|
+
function specForLane(lane) {
|
|
11265
|
+
const config = lane.data?.config ?? {};
|
|
11266
|
+
const text3 = (key2) => {
|
|
11267
|
+
const value = config[key2];
|
|
11268
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
11269
|
+
};
|
|
11270
|
+
const env = config.env;
|
|
11271
|
+
return {
|
|
11272
|
+
command: text3("command"),
|
|
11273
|
+
cwd: text3("cwd"),
|
|
11274
|
+
env: env && typeof env === "object" && !Array.isArray(env) ? Object.fromEntries(
|
|
11275
|
+
Object.entries(env).map(([k, v]) => [k, String(v)])
|
|
11276
|
+
) : void 0
|
|
11277
|
+
};
|
|
11278
|
+
}
|
|
11279
|
+
|
|
10492
11280
|
// src/runtime/workflow-props.ts
|
|
10493
11281
|
function typeOf(value) {
|
|
10494
11282
|
if (value === null) return "null";
|
|
@@ -10567,6 +11355,25 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
10567
11355
|
const props = propsCheck.props;
|
|
10568
11356
|
const declaresProps = (graph.inputs?.length ?? 0) > 0;
|
|
10569
11357
|
const incomingByNode = indexIncoming(graph.edges);
|
|
11358
|
+
const sessions = new TerminalSessions(graph);
|
|
11359
|
+
const isTerminalLane = (candidate) => getNodeKind(candidate.type ?? "")?.name === "@particle-academy/terminal_lane";
|
|
11360
|
+
const terminalAccessorFor = (node) => {
|
|
11361
|
+
const laneId = sessions.laneFor(node.id, isTerminalLane);
|
|
11362
|
+
if (laneId === null) return void 0;
|
|
11363
|
+
const lane = graph.nodes.find((n) => n.id === laneId);
|
|
11364
|
+
if (!lane) return void 0;
|
|
11365
|
+
return {
|
|
11366
|
+
session: () => sessions.session(laneId, specForLane(lane)),
|
|
11367
|
+
// Awaits the session first on purpose. A transcript handed out before
|
|
11368
|
+
// anything is feeding it would look perfectly healthy and match nothing,
|
|
11369
|
+
// and "waited and nothing came" is the hardest failure here to tell apart
|
|
11370
|
+
// from a process that is simply slow.
|
|
11371
|
+
transcript: async () => {
|
|
11372
|
+
await sessions.session(laneId, specForLane(lane));
|
|
11373
|
+
return sessions.transcriptFor(laneId);
|
|
11374
|
+
}
|
|
11375
|
+
};
|
|
11376
|
+
};
|
|
10570
11377
|
const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
|
|
10571
11378
|
onEvent({ type: "run-start" });
|
|
10572
11379
|
try {
|
|
@@ -10632,7 +11439,8 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
10632
11439
|
emit: onEvent,
|
|
10633
11440
|
executors,
|
|
10634
11441
|
depth,
|
|
10635
|
-
run
|
|
11442
|
+
run,
|
|
11443
|
+
terminal: terminalAccessorFor(node)
|
|
10636
11444
|
})
|
|
10637
11445
|
);
|
|
10638
11446
|
outputs[node.id] = result;
|
|
@@ -10654,6 +11462,9 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
10654
11462
|
}
|
|
10655
11463
|
} finally {
|
|
10656
11464
|
if (timer2) clearTimeout(timer2);
|
|
11465
|
+
for (const error of await sessions.closeAll()) {
|
|
11466
|
+
onEvent({ type: "log", level: "warn", message: `terminal close failed: ${error.message}` });
|
|
11467
|
+
}
|
|
10657
11468
|
}
|
|
10658
11469
|
const ok = errors.length === 0;
|
|
10659
11470
|
onEvent({ type: "run-end", ok });
|
|
@@ -10732,7 +11543,7 @@ function pickExecutor(executors, node) {
|
|
|
10732
11543
|
for (const id2 of executorLookupIds(node)) {
|
|
10733
11544
|
if (executors[id2]) return executors[id2];
|
|
10734
11545
|
}
|
|
10735
|
-
return
|
|
11546
|
+
return getNodeKind(node.type ?? "")?.executor;
|
|
10736
11547
|
}
|
|
10737
11548
|
function activatedPorts(node, result) {
|
|
10738
11549
|
if (result && typeof result === "object") {
|
|
@@ -10802,106 +11613,6 @@ function errorText(error) {
|
|
|
10802
11613
|
return error instanceof Error ? error.message : String(error);
|
|
10803
11614
|
}
|
|
10804
11615
|
|
|
10805
|
-
// src/expressions/expr.ts
|
|
10806
|
-
function wholeExpression(trimmed) {
|
|
10807
|
-
if (trimmed.length < 4) return null;
|
|
10808
|
-
if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
|
|
10809
|
-
return trimmed.slice(2, -2);
|
|
10810
|
-
}
|
|
10811
|
-
function interpolate(template, resolve) {
|
|
10812
|
-
let out = "";
|
|
10813
|
-
let i = 0;
|
|
10814
|
-
for (; ; ) {
|
|
10815
|
-
const open = template.indexOf("{{", i);
|
|
10816
|
-
if (open === -1) return out + template.slice(i);
|
|
10817
|
-
const close = template.indexOf("}}", open + 2);
|
|
10818
|
-
if (close === -1) return out + template.slice(i);
|
|
10819
|
-
out += template.slice(i, open) + resolve(template.slice(open + 2, close));
|
|
10820
|
-
i = close + 2;
|
|
10821
|
-
}
|
|
10822
|
-
}
|
|
10823
|
-
var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
|
|
10824
|
-
var UnresolvedPathError = class extends Error {
|
|
10825
|
-
constructor(path) {
|
|
10826
|
-
super(
|
|
10827
|
-
`Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
|
|
10828
|
-
);
|
|
10829
|
-
this.path = path;
|
|
10830
|
-
this.name = "UnresolvedPathError";
|
|
10831
|
-
}
|
|
10832
|
-
};
|
|
10833
|
-
function tryResolvePath(path, context) {
|
|
10834
|
-
const unresolved = { resolved: false, value: null };
|
|
10835
|
-
const trimmed = path.trim();
|
|
10836
|
-
if (trimmed === "") return unresolved;
|
|
10837
|
-
const segments = trimmed.split(".");
|
|
10838
|
-
let cursor;
|
|
10839
|
-
const head = segments[0];
|
|
10840
|
-
if (head === "$json" || head === "$input") {
|
|
10841
|
-
cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
|
|
10842
|
-
segments.shift();
|
|
10843
|
-
} else {
|
|
10844
|
-
cursor = context;
|
|
10845
|
-
}
|
|
10846
|
-
for (const segment of segments) {
|
|
10847
|
-
if (cursor === null || cursor === void 0) return unresolved;
|
|
10848
|
-
if (typeof cursor !== "object") return unresolved;
|
|
10849
|
-
const next = cursor[segment];
|
|
10850
|
-
if (next === void 0) return unresolved;
|
|
10851
|
-
cursor = next;
|
|
10852
|
-
}
|
|
10853
|
-
return { resolved: true, value: cursor === void 0 ? null : cursor };
|
|
10854
|
-
}
|
|
10855
|
-
function resolvePath(path, context) {
|
|
10856
|
-
return tryResolvePath(path, context).value;
|
|
10857
|
-
}
|
|
10858
|
-
function evaluateExpression(template, context, options = {}) {
|
|
10859
|
-
if (typeof template !== "string") return template;
|
|
10860
|
-
const policy = options.onUnresolved ?? "empty";
|
|
10861
|
-
const whole = wholeExpression(template.trim());
|
|
10862
|
-
if (whole !== null) {
|
|
10863
|
-
const r = tryResolvePath(whole, context);
|
|
10864
|
-
if (r.resolved) return r.value;
|
|
10865
|
-
if (policy === "throw") throw new UnresolvedPathError(whole);
|
|
10866
|
-
return policy === "keep" ? template : null;
|
|
10867
|
-
}
|
|
10868
|
-
return interpolate(template, (path) => {
|
|
10869
|
-
const r = tryResolvePath(path, context);
|
|
10870
|
-
if (r.resolved) return stringify(r.value);
|
|
10871
|
-
if (policy === "throw") throw new UnresolvedPathError(path);
|
|
10872
|
-
return policy === "keep" ? `{{${path}}}` : "";
|
|
10873
|
-
});
|
|
10874
|
-
}
|
|
10875
|
-
function truthy(value) {
|
|
10876
|
-
if (typeof value === "boolean") return value;
|
|
10877
|
-
if (value === null || value === void 0) return false;
|
|
10878
|
-
if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
|
|
10879
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
10880
|
-
if (typeof value === "number") return value !== 0;
|
|
10881
|
-
return Boolean(value);
|
|
10882
|
-
}
|
|
10883
|
-
function text(value) {
|
|
10884
|
-
return stringify(value);
|
|
10885
|
-
}
|
|
10886
|
-
function stringify(value) {
|
|
10887
|
-
if (typeof value === "string") return value;
|
|
10888
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
10889
|
-
if (value === null || value === void 0) return "";
|
|
10890
|
-
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
10891
|
-
try {
|
|
10892
|
-
return JSON.stringify(value) ?? "";
|
|
10893
|
-
} catch {
|
|
10894
|
-
return "";
|
|
10895
|
-
}
|
|
10896
|
-
}
|
|
10897
|
-
function evaluateConfig(config, context, options = {}) {
|
|
10898
|
-
const out = {};
|
|
10899
|
-
for (const [key2, value] of Object.entries(config)) {
|
|
10900
|
-
out[key2] = Array.isArray(value) ? value.map((v) => evaluateExpression(v, context, options)) : value !== null && typeof value === "object" ? evaluateConfig(value, context, options) : evaluateExpression(value, context, options);
|
|
10901
|
-
}
|
|
10902
|
-
return out;
|
|
10903
|
-
}
|
|
10904
|
-
|
|
10905
11616
|
// src/registry/pause.ts
|
|
10906
11617
|
var PAUSE_PREFIX = "fancy-flow:pause:";
|
|
10907
11618
|
var LEGACY_PAUSE_PREFIXES = [
|
|
@@ -11345,31 +12056,31 @@ async function runFixtures(file, executor) {
|
|
|
11345
12056
|
}
|
|
11346
12057
|
const result = await runFlow(graph, executors, (e) => events.push(e));
|
|
11347
12058
|
releaseStubs();
|
|
11348
|
-
const
|
|
12059
|
+
const fail2 = (message) => failures.push({ case: testCase.name, message });
|
|
11349
12060
|
const expected = testCase.expect;
|
|
11350
12061
|
let caseOk = true;
|
|
11351
12062
|
if (expected.pause) {
|
|
11352
12063
|
const paused = decodePause(result.error);
|
|
11353
12064
|
if (!paused) {
|
|
11354
12065
|
caseOk = false;
|
|
11355
|
-
|
|
12066
|
+
fail2(`expected a pause awaiting "${expected.pause.awaiting}", got ${result.ok ? "a completed run" : `error: ${result.error}`}`);
|
|
11356
12067
|
} else {
|
|
11357
12068
|
if (paused.awaiting !== expected.pause.awaiting) {
|
|
11358
12069
|
caseOk = false;
|
|
11359
|
-
|
|
12070
|
+
fail2(`expected pause awaiting "${expected.pause.awaiting}", got "${paused.awaiting}"`);
|
|
11360
12071
|
}
|
|
11361
12072
|
if ("detail" in expected.pause && !deepEqual(paused.detail, expected.pause.detail)) {
|
|
11362
12073
|
caseOk = false;
|
|
11363
|
-
|
|
12074
|
+
fail2(`pause detail mismatch: expected ${JSON.stringify(expected.pause.detail)}, got ${JSON.stringify(paused.detail)}`);
|
|
11364
12075
|
}
|
|
11365
12076
|
}
|
|
11366
12077
|
} else if (expected.error !== void 0) {
|
|
11367
12078
|
if (result.ok) {
|
|
11368
12079
|
caseOk = false;
|
|
11369
|
-
|
|
12080
|
+
fail2(`expected an error containing "${expected.error}", but the run succeeded`);
|
|
11370
12081
|
} else if (!String(result.error ?? "").includes(expected.error)) {
|
|
11371
12082
|
caseOk = false;
|
|
11372
|
-
|
|
12083
|
+
fail2(`expected an error containing "${expected.error}", got "${result.error}"`);
|
|
11373
12084
|
}
|
|
11374
12085
|
} else {
|
|
11375
12086
|
if (expected.ports !== void 0) {
|
|
@@ -11377,34 +12088,34 @@ async function runFixtures(file, executor) {
|
|
|
11377
12088
|
const want = [...expected.ports].sort();
|
|
11378
12089
|
if (!deepEqual(got, want)) {
|
|
11379
12090
|
caseOk = false;
|
|
11380
|
-
|
|
12091
|
+
fail2(
|
|
11381
12092
|
`expected these ports to reach a downstream node: [${want.join(", ")}], but [${got.join(", ")}] did` + (result.ok ? "" : ` (run error: ${result.error})`)
|
|
11382
12093
|
);
|
|
11383
12094
|
}
|
|
11384
12095
|
}
|
|
11385
12096
|
if ("value" in expected && !deepEqual(carried, expected.value)) {
|
|
11386
12097
|
caseOk = false;
|
|
11387
|
-
|
|
12098
|
+
fail2(`expected the value carried downstream to be ${JSON.stringify(expected.value)}, got ${JSON.stringify(carried)}`);
|
|
11388
12099
|
}
|
|
11389
12100
|
}
|
|
11390
12101
|
for (const want of expected.events ?? []) {
|
|
11391
12102
|
if (!events.some((e) => matchesEvent(e, want))) {
|
|
11392
12103
|
caseOk = false;
|
|
11393
|
-
|
|
12104
|
+
fail2(`expected an emitted event matching ${JSON.stringify(want)}, but none of the ${events.length} emitted events did`);
|
|
11394
12105
|
}
|
|
11395
12106
|
}
|
|
11396
12107
|
if (expected.afterResume) {
|
|
11397
12108
|
const paused = decodePause(result.error);
|
|
11398
12109
|
if (!paused) {
|
|
11399
12110
|
caseOk = false;
|
|
11400
|
-
|
|
12111
|
+
fail2("expected the run to pause before resuming, but it never paused");
|
|
11401
12112
|
} else {
|
|
11402
12113
|
const resumed = await resume(kindId, testCase, executor, paused.nodeId, expected.afterResume.submit);
|
|
11403
12114
|
const want = expected.afterResume;
|
|
11404
12115
|
if (want.error !== void 0) {
|
|
11405
12116
|
if (resumed.ok || !String(resumed.error ?? "").includes(want.error)) {
|
|
11406
12117
|
caseOk = false;
|
|
11407
|
-
|
|
12118
|
+
fail2(`after resume: expected an error containing "${want.error}", got ${resumed.ok ? "success" : `"${resumed.error}"`}`);
|
|
11408
12119
|
}
|
|
11409
12120
|
}
|
|
11410
12121
|
if (want.ports !== void 0) {
|
|
@@ -11412,12 +12123,12 @@ async function runFixtures(file, executor) {
|
|
|
11412
12123
|
const wanted = [...want.ports].sort();
|
|
11413
12124
|
if (!deepEqual(got, wanted)) {
|
|
11414
12125
|
caseOk = false;
|
|
11415
|
-
|
|
12126
|
+
fail2(`after resume: expected ports [${wanted.join(", ")}] to reach a downstream node, but [${got.join(", ")}] did`);
|
|
11416
12127
|
}
|
|
11417
12128
|
}
|
|
11418
12129
|
if ("value" in want && !deepEqual(resumed.carried, want.value)) {
|
|
11419
12130
|
caseOk = false;
|
|
11420
|
-
|
|
12131
|
+
fail2(`after resume: expected the value carried downstream to be ${JSON.stringify(want.value)}, got ${JSON.stringify(resumed.carried)}`);
|
|
11421
12132
|
}
|
|
11422
12133
|
}
|
|
11423
12134
|
}
|