@withone/cli 1.23.0 → 1.25.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.
|
@@ -447,6 +447,22 @@ var execAsync = promisify(exec);
|
|
|
447
447
|
function sleep2(ms) {
|
|
448
448
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
449
449
|
}
|
|
450
|
+
var StepTimeoutError = class extends Error {
|
|
451
|
+
errorCode = "TIMEOUT";
|
|
452
|
+
constructor(stepId, timeoutMs) {
|
|
453
|
+
super(`Step "${stepId}" exceeded timeout of ${timeoutMs}ms`);
|
|
454
|
+
this.name = "StepTimeoutError";
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
function withTimeout(promise, timeoutMs, stepId) {
|
|
458
|
+
let timer;
|
|
459
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
460
|
+
timer = setTimeout(() => reject(new StepTimeoutError(stepId, timeoutMs)), timeoutMs);
|
|
461
|
+
});
|
|
462
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
463
|
+
if (timer) clearTimeout(timer);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
450
466
|
function computeRetryDelay(onError, attempt) {
|
|
451
467
|
const base = onError.retryDelayMs ?? 1e3;
|
|
452
468
|
const max = onError.maxDelayMs ?? 3e4;
|
|
@@ -898,7 +914,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
898
914
|
if (flowStack.includes(resolvedKey)) {
|
|
899
915
|
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
900
916
|
}
|
|
901
|
-
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-
|
|
917
|
+
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-MXRNWG4Y.js");
|
|
902
918
|
const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
|
|
903
919
|
const subContext = await executeFlow(
|
|
904
920
|
subFlow,
|
|
@@ -910,10 +926,35 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
910
926
|
void 0,
|
|
911
927
|
[...flowStack, resolvedKey]
|
|
912
928
|
);
|
|
929
|
+
const finalStep = subFlow.steps[subFlow.steps.length - 1];
|
|
930
|
+
const finalOutput = finalStep ? subContext.steps[finalStep.id]?.output : void 0;
|
|
931
|
+
let flattenedOutput;
|
|
932
|
+
if (finalOutput && typeof finalOutput === "object" && !Array.isArray(finalOutput)) {
|
|
933
|
+
const collisions = Object.keys(finalOutput).filter(
|
|
934
|
+
(k) => k in subContext.steps
|
|
935
|
+
);
|
|
936
|
+
if (collisions.length > 0) {
|
|
937
|
+
options.onEvent?.({
|
|
938
|
+
event: "flow:warning",
|
|
939
|
+
message: `Sub-flow "${resolvedKey}" final step output fields [${collisions.join(", ")}] collide with sub-step ids \u2014 flattened fields take precedence.`
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
flattenedOutput = {
|
|
943
|
+
...subContext.steps,
|
|
944
|
+
...finalOutput,
|
|
945
|
+
_steps: subContext.steps
|
|
946
|
+
};
|
|
947
|
+
} else {
|
|
948
|
+
flattenedOutput = {
|
|
949
|
+
...subContext.steps,
|
|
950
|
+
_steps: subContext.steps,
|
|
951
|
+
...finalOutput !== void 0 ? { _finalOutput: finalOutput } : {}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
913
954
|
return {
|
|
914
955
|
status: "success",
|
|
915
|
-
output:
|
|
916
|
-
response:
|
|
956
|
+
output: flattenedOutput,
|
|
957
|
+
response: flattenedOutput
|
|
917
958
|
};
|
|
918
959
|
}
|
|
919
960
|
async function executePaginateStep(step, context, api, permissions, allowedActionIds, options) {
|
|
@@ -986,6 +1027,35 @@ async function executeBashStep(step, context, options) {
|
|
|
986
1027
|
response: { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 }
|
|
987
1028
|
};
|
|
988
1029
|
}
|
|
1030
|
+
function isMissing(value) {
|
|
1031
|
+
if (value === void 0 || value === null) return true;
|
|
1032
|
+
if (typeof value === "string" && value.length === 0) return true;
|
|
1033
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
function explainMissing(selector, context) {
|
|
1037
|
+
const parts = selector.slice(2).split(".");
|
|
1038
|
+
if (parts[0] !== "steps" || parts.length < 2) return "";
|
|
1039
|
+
const stepId = parts[1].replace(/\[.*$/, "");
|
|
1040
|
+
const upstream = context.steps[stepId];
|
|
1041
|
+
if (!upstream) return ` (upstream step "${stepId}" has not run)`;
|
|
1042
|
+
if (upstream.status === "skipped") return ` (upstream step "${stepId}" was skipped)`;
|
|
1043
|
+
if (upstream.status === "failed") return ` (upstream step "${stepId}" failed: ${upstream.error ?? "unknown error"})`;
|
|
1044
|
+
if (upstream.status === "timeout") return ` (upstream step "${stepId}" timed out)`;
|
|
1045
|
+
return "";
|
|
1046
|
+
}
|
|
1047
|
+
function checkRequires(step, context) {
|
|
1048
|
+
if (!step.requires || step.requires.length === 0) return;
|
|
1049
|
+
for (const selector of step.requires) {
|
|
1050
|
+
const value = resolveSelector(selector, context);
|
|
1051
|
+
if (isMissing(value)) {
|
|
1052
|
+
const why = explainMissing(selector, context);
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`Step "${step.id}" requires ${selector} but it resolved to ${value === void 0 ? "undefined" : value === null ? "null" : Array.isArray(value) ? "an empty array" : "an empty string"}${why}`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
989
1059
|
async function executeSingleStep(step, context, api, permissions, allowedActionIds, options, flowStack = []) {
|
|
990
1060
|
if (step.if) {
|
|
991
1061
|
const condResult = evaluateExpression(step.if, context);
|
|
@@ -1019,6 +1089,7 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1019
1089
|
});
|
|
1020
1090
|
await sleep2(delay);
|
|
1021
1091
|
}
|
|
1092
|
+
checkRequires(step, context);
|
|
1022
1093
|
if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
|
|
1023
1094
|
const resolvedConfig = step[step.type] ? resolveValue(step[step.type], context) : {};
|
|
1024
1095
|
options.onEvent?.({ event: "step:mock", stepId: step.id, type: step.type, config: resolvedConfig });
|
|
@@ -1031,47 +1102,37 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1031
1102
|
context.steps[step.id] = result2;
|
|
1032
1103
|
return result2;
|
|
1033
1104
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
break;
|
|
1066
|
-
case "paginate":
|
|
1067
|
-
result = await executePaginateStep(step, context, api, permissions, allowedActionIds, options);
|
|
1068
|
-
break;
|
|
1069
|
-
case "bash":
|
|
1070
|
-
result = await executeBashStep(step, context, options);
|
|
1071
|
-
break;
|
|
1072
|
-
default:
|
|
1073
|
-
throw new Error(`Unknown step type: ${step.type}`);
|
|
1074
|
-
}
|
|
1105
|
+
const dispatch = async () => {
|
|
1106
|
+
switch (step.type) {
|
|
1107
|
+
case "action":
|
|
1108
|
+
return await executeActionStep(step, context, api, permissions, allowedActionIds);
|
|
1109
|
+
case "transform":
|
|
1110
|
+
return executeTransformStep(step, context);
|
|
1111
|
+
case "code":
|
|
1112
|
+
return await executeCodeStep(step, context, options);
|
|
1113
|
+
case "condition":
|
|
1114
|
+
return await executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
1115
|
+
case "loop":
|
|
1116
|
+
return await executeLoopStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
1117
|
+
case "parallel":
|
|
1118
|
+
return await executeParallelStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
1119
|
+
case "file-read":
|
|
1120
|
+
return executeFileReadStep(step, context);
|
|
1121
|
+
case "file-write":
|
|
1122
|
+
return executeFileWriteStep(step, context);
|
|
1123
|
+
case "while":
|
|
1124
|
+
return await executeWhileStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
1125
|
+
case "flow":
|
|
1126
|
+
return await executeSubflowStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
1127
|
+
case "paginate":
|
|
1128
|
+
return await executePaginateStep(step, context, api, permissions, allowedActionIds, options);
|
|
1129
|
+
case "bash":
|
|
1130
|
+
return await executeBashStep(step, context, options);
|
|
1131
|
+
default:
|
|
1132
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
const result = step.timeoutMs ? await withTimeout(dispatch(), step.timeoutMs, step.id) : await dispatch();
|
|
1075
1136
|
result.durationMs = Date.now() - startTime;
|
|
1076
1137
|
if (attempt > 1) {
|
|
1077
1138
|
result.retries = attempt - 1;
|
|
@@ -1093,10 +1154,13 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1093
1154
|
const errorMessage = lastError?.message || "Unknown error";
|
|
1094
1155
|
const strategy = step.onError?.strategy || "fail";
|
|
1095
1156
|
const retriesUsed = Math.max(0, maxAttempts - 1);
|
|
1157
|
+
const isTimeout = lastError instanceof StepTimeoutError;
|
|
1158
|
+
const errorCode = lastError?.errorCode;
|
|
1096
1159
|
if (strategy === "continue") {
|
|
1097
1160
|
const result = {
|
|
1098
|
-
status: "failed",
|
|
1161
|
+
status: isTimeout ? "timeout" : "failed",
|
|
1099
1162
|
error: errorMessage,
|
|
1163
|
+
...errorCode ? { errorCode } : {},
|
|
1100
1164
|
durationMs: Date.now() - startTime,
|
|
1101
1165
|
retries: retriesUsed
|
|
1102
1166
|
};
|
|
@@ -1105,8 +1169,9 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1105
1169
|
}
|
|
1106
1170
|
if (strategy === "fallback" && step.onError?.fallbackStepId) {
|
|
1107
1171
|
const result = {
|
|
1108
|
-
status: "failed",
|
|
1172
|
+
status: isTimeout ? "timeout" : "failed",
|
|
1109
1173
|
error: errorMessage,
|
|
1174
|
+
...errorCode ? { errorCode } : {},
|
|
1110
1175
|
durationMs: Date.now() - startTime,
|
|
1111
1176
|
retries: retriesUsed
|
|
1112
1177
|
};
|
|
@@ -1243,7 +1308,9 @@ var FLOW_SCHEMA = {
|
|
|
1243
1308
|
name: { type: "string", required: true, description: "Human-readable step label" },
|
|
1244
1309
|
type: { type: "string", required: true, description: "Step type (determines which config object is required)" },
|
|
1245
1310
|
if: { type: "string", required: false, description: "JS expression \u2014 skip step if falsy" },
|
|
1246
|
-
unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" }
|
|
1311
|
+
unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" },
|
|
1312
|
+
timeoutMs: { type: "number", required: false, description: 'Wall-clock timeout (ms). On expiry the step fails with errorCode:"TIMEOUT"; with onError:continue the result gets status:"timeout".' },
|
|
1313
|
+
requires: { type: "array", required: false, description: "Presence preconditions: array of $.input.X or $.steps.X.output... selectors that must resolve to a non-empty value before the step runs. Failures honor onError." }
|
|
1247
1314
|
},
|
|
1248
1315
|
stepTypes: [
|
|
1249
1316
|
{
|
|
@@ -1751,6 +1818,39 @@ Every completed step produces both \`output\` and \`response\`:
|
|
|
1751
1818
|
- **Code/transform steps**: \`output\` is the return value. \`response\` is an alias for \`output\`.
|
|
1752
1819
|
- **In practice**: Use \`$.steps.stepId.response\` for action steps (API data) and \`$.steps.stepId.output\` for code/transform steps (computed data). Both work interchangeably, but using the semantically correct one makes flows easier to read.
|
|
1753
1820
|
|
|
1821
|
+
### Step result metadata (\`status\`, \`error\`, \`errorCode\`)
|
|
1822
|
+
|
|
1823
|
+
Every step result also exposes execution metadata that downstream steps can inspect:
|
|
1824
|
+
|
|
1825
|
+
| Field | Values | When set |
|
|
1826
|
+
|-------|--------|----------|
|
|
1827
|
+
| \`$.steps.X.status\` | \`"success"\` \\| \`"skipped"\` \\| \`"failed"\` \\| \`"timeout"\` | Always |
|
|
1828
|
+
| \`$.steps.X.error\` | error message string | When status is \`failed\` or \`timeout\` |
|
|
1829
|
+
| \`$.steps.X.errorCode\` | machine-readable code (e.g. \`"TIMEOUT"\`) | When the error has a code |
|
|
1830
|
+
| \`$.steps.X.durationMs\` | number | Always |
|
|
1831
|
+
| \`$.steps.X.retries\` | number | When the step was retried |
|
|
1832
|
+
|
|
1833
|
+
This lets downstream steps distinguish \`skipped\` (\`if\` condition false) from \`failed\` (error, \`onError:continue\`) from \`timeout\` (exceeded \`timeoutMs\`) \u2014 e.g. \`"if": "$.steps.enrichment.status === 'timeout'"\` to retry with a longer window.
|
|
1834
|
+
|
|
1835
|
+
### Sub-flow output (flattened)
|
|
1836
|
+
|
|
1837
|
+
When a step has \`type: "flow"\`, the sub-flow's final step output is flattened onto the parent step's \`output\`:
|
|
1838
|
+
|
|
1839
|
+
\`\`\`jsonc
|
|
1840
|
+
// Sub-flow "sub-consts" has a final step "load" that returns { CHART_URL, API_KEY }
|
|
1841
|
+
|
|
1842
|
+
// Preferred (flattened):
|
|
1843
|
+
"{{$.steps.loadConfig.output.CHART_URL}}"
|
|
1844
|
+
|
|
1845
|
+
// Legacy nested path (still works for backward compatibility):
|
|
1846
|
+
"{{$.steps.loadConfig.output.load.output.CHART_URL}}"
|
|
1847
|
+
|
|
1848
|
+
// Escape hatch for programmatic access to the full sub-flow steps map:
|
|
1849
|
+
"{{$.steps.loadConfig.output._steps.load.output.CHART_URL}}"
|
|
1850
|
+
\`\`\`
|
|
1851
|
+
|
|
1852
|
+
If a sub-step id collides with a flattened field name, the flattened field wins and the engine emits a \`flow:warning\` event.
|
|
1853
|
+
|
|
1754
1854
|
## Error Handling
|
|
1755
1855
|
|
|
1756
1856
|
\`\`\`json
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
loadFlowWithMeta,
|
|
17
17
|
resolveFlowPath,
|
|
18
18
|
saveFlow
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ONGJ2QKB.js";
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -2320,6 +2320,20 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
2320
2320
|
errors.push({ path: `${path8}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
|
|
2321
2321
|
continue;
|
|
2322
2322
|
}
|
|
2323
|
+
if (s.requires !== void 0) {
|
|
2324
|
+
if (!Array.isArray(s.requires)) {
|
|
2325
|
+
errors.push({ path: `${path8}.requires`, message: '"requires" must be an array of selector strings (e.g. ["$.steps.foo.output.bar"])' });
|
|
2326
|
+
} else {
|
|
2327
|
+
for (let r = 0; r < s.requires.length; r++) {
|
|
2328
|
+
const sel = s.requires[r];
|
|
2329
|
+
if (typeof sel !== "string") {
|
|
2330
|
+
errors.push({ path: `${path8}.requires[${r}]`, message: '"requires" entry must be a selector string' });
|
|
2331
|
+
} else if (!sel.startsWith("$.")) {
|
|
2332
|
+
errors.push({ path: `${path8}.requires[${r}]`, message: `"requires" entry "${sel}" must be a selector starting with "$." (e.g. "$.steps.foo.output.bar")` });
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2323
2337
|
if (s.onError && typeof s.onError === "object") {
|
|
2324
2338
|
const oe = s.onError;
|
|
2325
2339
|
if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
|
|
@@ -2529,6 +2543,14 @@ function validateSelectorReferences(flow2) {
|
|
|
2529
2543
|
function checkStep(step, pathPrefix, preceding2) {
|
|
2530
2544
|
if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`, preceding2);
|
|
2531
2545
|
if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`, preceding2);
|
|
2546
|
+
if (Array.isArray(step.requires)) {
|
|
2547
|
+
const reqs = step.requires;
|
|
2548
|
+
reqs.forEach((sel, i) => {
|
|
2549
|
+
if (typeof sel === "string" && sel.startsWith("$.")) {
|
|
2550
|
+
checkSelectors([sel], `${pathPrefix}.requires[${i}]`, preceding2);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2532
2554
|
const descriptor = getStepTypeDescriptor(step.type);
|
|
2533
2555
|
if (descriptor) {
|
|
2534
2556
|
const config2 = step[descriptor.configKey];
|
package/package.json
CHANGED
|
@@ -424,6 +424,35 @@ Alternatively, pass values as environment variables (also shell-safe) and refere
|
|
|
424
424
|
}
|
|
425
425
|
```
|
|
426
426
|
|
|
427
|
+
## Step Input Contracts (`requires`)
|
|
428
|
+
|
|
429
|
+
Declare the data a step depends on so the engine fails fast — with a useful error — when an upstream value is missing. Without `requires`, a skipped or failed upstream step silently leaves `undefined` in the context and the consumer either crashes deep in user code or burns an LLM call on empty input.
|
|
430
|
+
|
|
431
|
+
```json
|
|
432
|
+
{
|
|
433
|
+
"id": "summarizeFounder",
|
|
434
|
+
"type": "code",
|
|
435
|
+
"requires": [
|
|
436
|
+
"$.steps.fetchProfile.output.bio",
|
|
437
|
+
"$.input.founderName"
|
|
438
|
+
],
|
|
439
|
+
"code": { "module": "lib/summarize.mjs" }
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Each entry is a `$.input.X` or `$.steps.X.output...` selector. A selector is considered missing when it resolves to `undefined`, `null`, `""`, or `[]` (empty objects are allowed). On a miss, the engine throws **before** the step runs:
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
Step "summarizeFounder" requires $.steps.fetchProfile.output.bio but it
|
|
447
|
+
resolved to undefined (upstream step "fetchProfile" was skipped)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The "because…" suffix tells you exactly why — skipped, failed, or timed out — so you can fix the upstream wiring instead of guessing.
|
|
451
|
+
|
|
452
|
+
`requires` failures honor the step's `onError` strategy: pair `requires` with `onError: { strategy: "continue" }` to skip optional consumers gracefully, or leave the default `fail` to halt the flow on contract violations.
|
|
453
|
+
|
|
454
|
+
Forward references are caught at flow load time: if `requires` points at a step declared after the current step, validation rejects the flow.
|
|
455
|
+
|
|
427
456
|
## Error Handling
|
|
428
457
|
|
|
429
458
|
```json
|