@duckflux/core 0.6.8
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/dist/cel/index.js +12010 -0
- package/dist/engine/index.js +27937 -0
- package/dist/eventhub/index.js +151 -0
- package/dist/index.js +28044 -0
- package/dist/parser/index.js +26765 -0
- package/package.json +48 -0
- package/src/cel/index.ts +156 -0
- package/src/engine/control.ts +169 -0
- package/src/engine/engine.ts +127 -0
- package/src/engine/errors.ts +90 -0
- package/src/engine/index.ts +8 -0
- package/src/engine/output.ts +109 -0
- package/src/engine/sequential.ts +379 -0
- package/src/engine/state.ts +185 -0
- package/src/engine/timeout.ts +43 -0
- package/src/engine/wait.ts +102 -0
- package/src/eventhub/index.ts +24 -0
- package/src/eventhub/memory.ts +106 -0
- package/src/eventhub/types.ts +17 -0
- package/src/index.ts +51 -0
- package/src/model/index.ts +183 -0
- package/src/parser/index.ts +4 -0
- package/src/parser/parser.ts +13 -0
- package/src/parser/schema/duckflux.schema.json +573 -0
- package/src/parser/schema.ts +26 -0
- package/src/parser/validate.ts +541 -0
- package/src/parser/validate_inputs.ts +187 -0
- package/src/participant/emit.ts +63 -0
- package/src/participant/exec.ts +158 -0
- package/src/participant/http.ts +45 -0
- package/src/participant/index.ts +61 -0
- package/src/participant/mcp.ts +8 -0
- package/src/participant/workflow.ts +73 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseDuration } from "./errors";
|
|
2
|
+
|
|
3
|
+
export class TimeoutError extends Error {
|
|
4
|
+
timeoutMs: number;
|
|
5
|
+
constructor(message: string, timeoutMs: number) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "TimeoutError";
|
|
8
|
+
this.timeoutMs = timeoutMs;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function withTimeout<T>(fn: () => Promise<T>, timeoutMs: number): Promise<T> {
|
|
13
|
+
let timer: NodeJS.Timeout | undefined;
|
|
14
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
15
|
+
timer = setTimeout(() => reject(new TimeoutError(`operation timed out after ${timeoutMs}ms`, timeoutMs)), timeoutMs);
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
return await Promise.race([fn(), timeoutPromise]) as T;
|
|
19
|
+
} finally {
|
|
20
|
+
if (timer) clearTimeout(timer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveTimeout(
|
|
25
|
+
flowOverride?: { timeout?: string } | null,
|
|
26
|
+
participant?: { timeout?: string } | null,
|
|
27
|
+
defaults?: { timeout?: string } | null
|
|
28
|
+
): number | undefined {
|
|
29
|
+
const t = (flowOverride && flowOverride.timeout) ?? (participant && participant.timeout) ?? (defaults && defaults.timeout);
|
|
30
|
+
if (!t) return undefined;
|
|
31
|
+
try {
|
|
32
|
+
return parseDuration(t);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// If parsing fails, rethrow as Error with context
|
|
35
|
+
throw new Error(`invalid timeout value '${t}': ${(err && (err as Error).message) || err}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
TimeoutError,
|
|
41
|
+
withTimeout,
|
|
42
|
+
resolveTimeout,
|
|
43
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { evaluateCel } from "../cel/index";
|
|
2
|
+
import { parseDuration } from "./errors";
|
|
3
|
+
import type { WorkflowState } from "./state";
|
|
4
|
+
|
|
5
|
+
function sleep(ms: number): Promise<void> {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function executeWait(
|
|
10
|
+
state: WorkflowState,
|
|
11
|
+
waitDef: {
|
|
12
|
+
event?: string;
|
|
13
|
+
match?: string;
|
|
14
|
+
until?: string;
|
|
15
|
+
poll?: string;
|
|
16
|
+
timeout?: string;
|
|
17
|
+
onTimeout?: string;
|
|
18
|
+
},
|
|
19
|
+
chain?: unknown,
|
|
20
|
+
hub?: { subscribe(event: string, signal?: AbortSignal): AsyncIterable<{ name: string; payload: unknown }> },
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
): Promise<unknown> {
|
|
23
|
+
const timeoutMs = waitDef.timeout ? parseDuration(waitDef.timeout) : undefined;
|
|
24
|
+
const onTimeout = waitDef.onTimeout ?? "fail";
|
|
25
|
+
|
|
26
|
+
// Sleep mode: only timeout, no event or until
|
|
27
|
+
if (!waitDef.event && !waitDef.until && waitDef.timeout) {
|
|
28
|
+
await sleep(parseDuration(waitDef.timeout));
|
|
29
|
+
return chain;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Polling mode: until + optional poll + optional timeout
|
|
33
|
+
if (waitDef.until) {
|
|
34
|
+
const pollMs = waitDef.poll ? parseDuration(waitDef.poll) : 1000;
|
|
35
|
+
const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
36
|
+
|
|
37
|
+
while (true) {
|
|
38
|
+
if (signal?.aborted) {
|
|
39
|
+
throw new Error("wait aborted");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = evaluateCel(waitDef.until, state.toCelContext());
|
|
43
|
+
if (result === true) {
|
|
44
|
+
return chain;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (deadline && Date.now() >= deadline) {
|
|
48
|
+
if (onTimeout === "skip") return chain;
|
|
49
|
+
throw new Error(`wait polling timed out after ${waitDef.timeout}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await sleep(pollMs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Event mode: event + optional match + optional timeout
|
|
57
|
+
if (waitDef.event) {
|
|
58
|
+
if (!hub) {
|
|
59
|
+
throw new Error("wait.event requires an event hub but none was provided");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const combinedSignal = signal
|
|
64
|
+
? AbortSignal.any([signal, controller.signal])
|
|
65
|
+
: controller.signal;
|
|
66
|
+
|
|
67
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
if (timeoutMs) {
|
|
69
|
+
timeoutTimer = setTimeout(() => controller.abort(), timeoutMs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
for await (const envelope of hub.subscribe(waitDef.event, combinedSignal)) {
|
|
74
|
+
if (waitDef.match) {
|
|
75
|
+
// Evaluate match condition with event payload in context
|
|
76
|
+
const prevEvent = state.eventPayload;
|
|
77
|
+
state.eventPayload = envelope.payload;
|
|
78
|
+
const matched = evaluateCel(waitDef.match, state.toCelContext());
|
|
79
|
+
if (matched !== true) {
|
|
80
|
+
// Restore previous event payload on non-match
|
|
81
|
+
state.eventPayload = prevEvent;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Event matched
|
|
87
|
+
state.eventPayload = envelope.payload;
|
|
88
|
+
return envelope.payload;
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if ((err as Error)?.name === "AbortError" || controller.signal.aborted) {
|
|
92
|
+
if (onTimeout === "skip") return chain;
|
|
93
|
+
throw new Error(`wait.event timed out after ${waitDef.timeout}`);
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
96
|
+
} finally {
|
|
97
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return chain;
|
|
102
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type { EventHub, EventEnvelope, EventHubConfig } from "./types";
|
|
2
|
+
export { MemoryHub } from "./memory";
|
|
3
|
+
|
|
4
|
+
import type { EventHub, EventHubConfig } from "./types";
|
|
5
|
+
import { MemoryHub } from "./memory";
|
|
6
|
+
|
|
7
|
+
export async function createHub(config: EventHubConfig): Promise<EventHub> {
|
|
8
|
+
switch (config.backend) {
|
|
9
|
+
case "memory":
|
|
10
|
+
return new MemoryHub();
|
|
11
|
+
case "nats":
|
|
12
|
+
throw new Error(
|
|
13
|
+
"NATS backend has been moved to @duckflux/hub-nats. " +
|
|
14
|
+
"Install it and pass a NatsHub instance via ExecuteOptions.hub instead.",
|
|
15
|
+
);
|
|
16
|
+
case "redis":
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Redis backend has been moved to @duckflux/hub-redis. " +
|
|
19
|
+
"Install it and pass a RedisHub instance via ExecuteOptions.hub instead.",
|
|
20
|
+
);
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(`unknown event hub backend: ${(config as { backend: string }).backend}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { EventEnvelope, EventHub } from "./types";
|
|
2
|
+
|
|
3
|
+
type Listener = (envelope: EventEnvelope) => void;
|
|
4
|
+
|
|
5
|
+
export class MemoryHub implements EventHub {
|
|
6
|
+
private listeners = new Map<string, Set<Listener>>();
|
|
7
|
+
private buffer = new Map<string, EventEnvelope[]>();
|
|
8
|
+
private closed = false;
|
|
9
|
+
|
|
10
|
+
async publish(event: string, payload: unknown): Promise<void> {
|
|
11
|
+
if (this.closed) throw new Error("hub is closed");
|
|
12
|
+
|
|
13
|
+
const envelope: EventEnvelope = { name: event, payload };
|
|
14
|
+
|
|
15
|
+
// Buffer for replay
|
|
16
|
+
let buf = this.buffer.get(event);
|
|
17
|
+
if (!buf) {
|
|
18
|
+
buf = [];
|
|
19
|
+
this.buffer.set(event, buf);
|
|
20
|
+
}
|
|
21
|
+
buf.push(envelope);
|
|
22
|
+
|
|
23
|
+
// Fan-out to current listeners
|
|
24
|
+
const listeners = this.listeners.get(event);
|
|
25
|
+
if (listeners) {
|
|
26
|
+
for (const listener of listeners) {
|
|
27
|
+
listener(envelope);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async publishAndWaitAck(event: string, payload: unknown, _timeoutMs: number): Promise<void> {
|
|
33
|
+
// In-memory delivery is synchronous, so ack is immediate
|
|
34
|
+
await this.publish(event, payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async *subscribe(event: string, signal?: AbortSignal): AsyncIterable<EventEnvelope> {
|
|
38
|
+
if (this.closed) return;
|
|
39
|
+
|
|
40
|
+
// Replay buffered events first
|
|
41
|
+
const buffered = this.buffer.get(event);
|
|
42
|
+
if (buffered) {
|
|
43
|
+
for (const envelope of buffered) {
|
|
44
|
+
if (signal?.aborted) return;
|
|
45
|
+
yield envelope;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Then listen for new events
|
|
50
|
+
const queue: EventEnvelope[] = [];
|
|
51
|
+
let resolve: (() => void) | null = null;
|
|
52
|
+
|
|
53
|
+
const listener: Listener = (envelope) => {
|
|
54
|
+
queue.push(envelope);
|
|
55
|
+
if (resolve) {
|
|
56
|
+
resolve();
|
|
57
|
+
resolve = null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let listeners = this.listeners.get(event);
|
|
62
|
+
if (!listeners) {
|
|
63
|
+
listeners = new Set();
|
|
64
|
+
this.listeners.set(event, listeners);
|
|
65
|
+
}
|
|
66
|
+
listeners.add(listener);
|
|
67
|
+
|
|
68
|
+
const onAbort = () => {
|
|
69
|
+
listeners!.delete(listener);
|
|
70
|
+
if (resolve) {
|
|
71
|
+
resolve();
|
|
72
|
+
resolve = null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (signal) {
|
|
77
|
+
signal.addEventListener("abort", onAbort);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
while (!this.closed && !signal?.aborted) {
|
|
82
|
+
if (queue.length > 0) {
|
|
83
|
+
yield queue.shift()!;
|
|
84
|
+
} else {
|
|
85
|
+
await new Promise<void>((r) => {
|
|
86
|
+
resolve = r;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
listeners.delete(listener);
|
|
92
|
+
if (signal) {
|
|
93
|
+
signal.removeEventListener("abort", onAbort);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async close(): Promise<void> {
|
|
99
|
+
this.closed = true;
|
|
100
|
+
// Wake up all waiting subscribers
|
|
101
|
+
for (const listeners of this.listeners.values()) {
|
|
102
|
+
listeners.clear();
|
|
103
|
+
}
|
|
104
|
+
this.listeners.clear();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface EventEnvelope {
|
|
2
|
+
name: string;
|
|
3
|
+
payload: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface EventHubConfig {
|
|
7
|
+
backend: "memory" | "nats" | "redis";
|
|
8
|
+
nats?: { url: string; stream?: string };
|
|
9
|
+
redis?: { addr?: string; db?: number; consumerGroup?: string };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EventHub {
|
|
13
|
+
publish(event: string, payload: unknown): Promise<void>;
|
|
14
|
+
publishAndWaitAck(event: string, payload: unknown, timeoutMs: number): Promise<void>;
|
|
15
|
+
subscribe(event: string, signal?: AbortSignal): AsyncIterable<EventEnvelope>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Parser
|
|
2
|
+
export { parseWorkflow, parseWorkflowFile } from "./parser/parser";
|
|
3
|
+
export { validateSchema } from "./parser/schema";
|
|
4
|
+
export { validateSemantic } from "./parser/validate";
|
|
5
|
+
export { validateInputs } from "./parser/validate_inputs";
|
|
6
|
+
|
|
7
|
+
// CEL
|
|
8
|
+
export { validateCelExpression, evaluateCel, evaluateCelStrict, buildCelContext, evalMaybeCel } from "./cel/index";
|
|
9
|
+
|
|
10
|
+
// Engine
|
|
11
|
+
export { executeWorkflow, runWorkflowFromFile } from "./engine/engine";
|
|
12
|
+
export type { EventHub as EngineEventHub, ExecuteOptions } from "./engine/engine";
|
|
13
|
+
export { WorkflowState } from "./engine/state";
|
|
14
|
+
export { mergeChainedInput } from "./engine/sequential";
|
|
15
|
+
export { executeWait } from "./engine/wait";
|
|
16
|
+
|
|
17
|
+
// Event Hub
|
|
18
|
+
export { MemoryHub } from "./eventhub/memory";
|
|
19
|
+
export { createHub } from "./eventhub/index";
|
|
20
|
+
export type { EventHub, EventEnvelope, EventHubConfig } from "./eventhub/types";
|
|
21
|
+
|
|
22
|
+
// Participants
|
|
23
|
+
export { executeEmit } from "./participant/emit";
|
|
24
|
+
|
|
25
|
+
// Types
|
|
26
|
+
export type {
|
|
27
|
+
Workflow,
|
|
28
|
+
Participant,
|
|
29
|
+
ParticipantBase,
|
|
30
|
+
FlowStep,
|
|
31
|
+
ExecParticipant,
|
|
32
|
+
HttpParticipant,
|
|
33
|
+
McpParticipant,
|
|
34
|
+
WorkflowParticipant,
|
|
35
|
+
EmitParticipant,
|
|
36
|
+
InlineParticipant,
|
|
37
|
+
WaitStep,
|
|
38
|
+
LoopStep,
|
|
39
|
+
ParallelStep,
|
|
40
|
+
IfStep,
|
|
41
|
+
FlowStepOverride,
|
|
42
|
+
StepResult,
|
|
43
|
+
WorkflowResult,
|
|
44
|
+
WorkflowOutput,
|
|
45
|
+
ValidationResult,
|
|
46
|
+
ValidationError,
|
|
47
|
+
WorkflowDefaults,
|
|
48
|
+
ErrorStrategy,
|
|
49
|
+
RetryConfig,
|
|
50
|
+
InputDefinition,
|
|
51
|
+
} from "./model/index";
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export interface Workflow {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
version?: string | number;
|
|
5
|
+
defaults?: WorkflowDefaults;
|
|
6
|
+
inputs?: Record<string, InputDefinition | null>;
|
|
7
|
+
participants?: Record<string, Participant>;
|
|
8
|
+
flow: FlowStep[];
|
|
9
|
+
output?: WorkflowOutput;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WorkflowDefaults {
|
|
13
|
+
timeout?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
onError?: ErrorStrategy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ErrorStrategy = "fail" | "skip" | "retry" | string;
|
|
19
|
+
|
|
20
|
+
export interface RetryConfig {
|
|
21
|
+
max: number;
|
|
22
|
+
backoff?: string;
|
|
23
|
+
factor?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ParticipantBase {
|
|
27
|
+
type: string;
|
|
28
|
+
as?: string;
|
|
29
|
+
timeout?: string;
|
|
30
|
+
onError?: ErrorStrategy;
|
|
31
|
+
retry?: RetryConfig;
|
|
32
|
+
input?: string | Record<string, string>;
|
|
33
|
+
output?: Record<string, unknown>;
|
|
34
|
+
when?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExecParticipant extends ParticipantBase {
|
|
38
|
+
type: "exec";
|
|
39
|
+
run?: string;
|
|
40
|
+
env?: Record<string, string>;
|
|
41
|
+
cwd?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface HttpParticipant extends ParticipantBase {
|
|
45
|
+
type: "http";
|
|
46
|
+
method?: string;
|
|
47
|
+
url: string;
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
body?: string | Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface McpParticipant extends ParticipantBase {
|
|
53
|
+
type: "mcp";
|
|
54
|
+
server?: string;
|
|
55
|
+
tool?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WorkflowParticipant extends ParticipantBase {
|
|
59
|
+
type: "workflow";
|
|
60
|
+
path: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface EmitParticipant extends ParticipantBase {
|
|
64
|
+
type: "emit";
|
|
65
|
+
event: string;
|
|
66
|
+
payload?: string | Record<string, unknown>;
|
|
67
|
+
ack?: boolean;
|
|
68
|
+
onTimeout?: "fail" | "skip";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type Participant =
|
|
72
|
+
| ExecParticipant
|
|
73
|
+
| HttpParticipant
|
|
74
|
+
| McpParticipant
|
|
75
|
+
| WorkflowParticipant
|
|
76
|
+
| EmitParticipant;
|
|
77
|
+
|
|
78
|
+
export type InlineParticipant = Participant & {
|
|
79
|
+
as?: string;
|
|
80
|
+
when?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export interface WaitStep {
|
|
84
|
+
wait: {
|
|
85
|
+
event?: string;
|
|
86
|
+
match?: string;
|
|
87
|
+
until?: string;
|
|
88
|
+
poll?: string;
|
|
89
|
+
timeout?: string;
|
|
90
|
+
onTimeout?: string;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SetStep {
|
|
95
|
+
set: Record<string, string>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type FlowStep = string | FlowStepOverride | LoopStep | ParallelStep | IfStep | WaitStep | SetStep | InlineParticipant;
|
|
99
|
+
|
|
100
|
+
export interface FlowStepOverride {
|
|
101
|
+
[participantName: string]: {
|
|
102
|
+
timeout?: string;
|
|
103
|
+
onError?: ErrorStrategy;
|
|
104
|
+
when?: string;
|
|
105
|
+
input?: string | Record<string, string>;
|
|
106
|
+
retry?: RetryConfig;
|
|
107
|
+
workflow?: string;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface LoopStep {
|
|
112
|
+
loop: {
|
|
113
|
+
as?: string;
|
|
114
|
+
until?: string;
|
|
115
|
+
max?: number | string;
|
|
116
|
+
steps: FlowStep[];
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ParallelStep {
|
|
121
|
+
parallel: FlowStep[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface IfStep {
|
|
125
|
+
if: {
|
|
126
|
+
condition: string;
|
|
127
|
+
then: FlowStep[];
|
|
128
|
+
else?: FlowStep[];
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface InputDefinition {
|
|
133
|
+
type?: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
required?: boolean;
|
|
136
|
+
default?: unknown;
|
|
137
|
+
format?: string;
|
|
138
|
+
enum?: unknown[];
|
|
139
|
+
minimum?: number;
|
|
140
|
+
maximum?: number;
|
|
141
|
+
minLength?: number;
|
|
142
|
+
maxLength?: number;
|
|
143
|
+
pattern?: string;
|
|
144
|
+
items?: InputDefinition;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type WorkflowOutput =
|
|
148
|
+
| string
|
|
149
|
+
| Record<string, string>
|
|
150
|
+
| { schema: Record<string, InputDefinition>; map: Record<string, string> };
|
|
151
|
+
|
|
152
|
+
export interface StepResult {
|
|
153
|
+
status: "success" | "failure" | "skipped";
|
|
154
|
+
output: string;
|
|
155
|
+
parsedOutput?: unknown;
|
|
156
|
+
error?: string;
|
|
157
|
+
duration: number;
|
|
158
|
+
startedAt?: string;
|
|
159
|
+
finishedAt?: string;
|
|
160
|
+
retries?: number;
|
|
161
|
+
cwd?: string;
|
|
162
|
+
/** HTTP status code on HTTP participant failure */
|
|
163
|
+
httpStatus?: number;
|
|
164
|
+
/** HTTP response body on HTTP participant failure */
|
|
165
|
+
responseBody?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface WorkflowResult {
|
|
169
|
+
success: boolean;
|
|
170
|
+
output: unknown;
|
|
171
|
+
steps: Record<string, StepResult>;
|
|
172
|
+
duration: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ValidationResult {
|
|
176
|
+
valid: boolean;
|
|
177
|
+
errors: ValidationError[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ValidationError {
|
|
181
|
+
path: string;
|
|
182
|
+
message: string;
|
|
183
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import yaml from "yaml";
|
|
3
|
+
import type { Workflow } from "../model/index";
|
|
4
|
+
|
|
5
|
+
export function parseWorkflow(yamlContent: string): Workflow {
|
|
6
|
+
const parsed = yaml.parse(yamlContent) as Workflow;
|
|
7
|
+
return parsed;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function parseWorkflowFile(filePath: string): Promise<Workflow> {
|
|
11
|
+
const content = await readFile(filePath, "utf-8");
|
|
12
|
+
return parseWorkflow(content);
|
|
13
|
+
}
|