@withone/cli 1.22.0 → 1.23.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
|
+
function computeRetryDelay(onError, attempt) {
|
|
451
|
+
const base = onError.retryDelayMs ?? 1e3;
|
|
452
|
+
const max = onError.maxDelayMs ?? 3e4;
|
|
453
|
+
const backoff = onError.backoff ?? "fixed";
|
|
454
|
+
const retryIndex = attempt - 2;
|
|
455
|
+
let delay;
|
|
456
|
+
if (backoff === "exponential" || backoff === "exponential-jitter") {
|
|
457
|
+
delay = Math.min(base * Math.pow(2, retryIndex), max);
|
|
458
|
+
if (backoff === "exponential-jitter") {
|
|
459
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
delay = base;
|
|
463
|
+
}
|
|
464
|
+
return Math.round(delay);
|
|
465
|
+
}
|
|
450
466
|
function resolveSelector(selectorPath, context) {
|
|
451
467
|
if (!selectorPath.startsWith("$.")) return selectorPath;
|
|
452
468
|
const parts = selectorPath.slice(2).split(/\.|\[/).map((p) => p.replace(/\]$/, ""));
|
|
@@ -474,12 +490,22 @@ function resolveSelector(selectorPath, context) {
|
|
|
474
490
|
}
|
|
475
491
|
return current;
|
|
476
492
|
}
|
|
493
|
+
function shellQuote(value) {
|
|
494
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
495
|
+
}
|
|
477
496
|
function interpolateString(str, context) {
|
|
478
|
-
return str.replace(/\{\{(\$\.[^}]+)\}\}/g, (_match, selector) => {
|
|
497
|
+
return str.replace(/\{\{\s*(q\s+)?(\$\.[^}\s]+)\s*\}\}/g, (_match, qFlag, selector) => {
|
|
479
498
|
const value = resolveSelector(selector, context);
|
|
480
|
-
if (value === void 0 || value === null) return "";
|
|
481
|
-
if (typeof value === "object")
|
|
482
|
-
|
|
499
|
+
if (value === void 0 || value === null) return qFlag ? `''` : "";
|
|
500
|
+
if (typeof value === "object") {
|
|
501
|
+
console.warn(
|
|
502
|
+
`[flow] WARNING: Handlebars expression "{{${qFlag ? "q " : ""}${selector}}}" resolved to ${Array.isArray(value) ? "an array" : "an object"} and was stringified as JSON. To pass objects/arrays as native values, use a direct selector without {{ }}: "${selector}"`
|
|
503
|
+
);
|
|
504
|
+
const json = JSON.stringify(value);
|
|
505
|
+
return qFlag ? shellQuote(json) : json;
|
|
506
|
+
}
|
|
507
|
+
const str2 = String(value);
|
|
508
|
+
return qFlag ? shellQuote(str2) : str2;
|
|
483
509
|
});
|
|
484
510
|
}
|
|
485
511
|
function resolveValue(value, context) {
|
|
@@ -487,7 +513,7 @@ function resolveValue(value, context) {
|
|
|
487
513
|
if (value.startsWith("$.") && !value.includes("{{")) {
|
|
488
514
|
return resolveSelector(value, context);
|
|
489
515
|
}
|
|
490
|
-
if (value.includes("{{$.")) {
|
|
516
|
+
if (value.includes("{{$.") || /\{\{\s*q\s+\$\./.test(value)) {
|
|
491
517
|
return interpolateString(value, context);
|
|
492
518
|
}
|
|
493
519
|
return value;
|
|
@@ -609,8 +635,8 @@ function executeTransformStep(step, context) {
|
|
|
609
635
|
async function executeCodeStep(step, context, options) {
|
|
610
636
|
const config = step.code;
|
|
611
637
|
if (config.module) {
|
|
612
|
-
const
|
|
613
|
-
return { status: "success", output
|
|
638
|
+
const output = await executeCodeModule(step.id, config.module, context, options);
|
|
639
|
+
return { status: "success", output, response: output };
|
|
614
640
|
}
|
|
615
641
|
if (typeof config.source !== "string") {
|
|
616
642
|
throw new Error(`Code step "${step.id}" must define either "source" or "module"`);
|
|
@@ -618,9 +644,41 @@ async function executeCodeStep(step, context, options) {
|
|
|
618
644
|
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
619
645
|
}).constructor;
|
|
620
646
|
const sandboxedRequire = createSandboxedRequire();
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
|
|
647
|
+
const sourceURL = `code:${step.id}`;
|
|
648
|
+
const taggedSource = `${config.source}
|
|
649
|
+
//# sourceURL=${sourceURL}`;
|
|
650
|
+
const fn = new AsyncFunction("$", "require", taggedSource);
|
|
651
|
+
try {
|
|
652
|
+
const output = await fn(context, sandboxedRequire);
|
|
653
|
+
return { status: "success", output, response: output };
|
|
654
|
+
} catch (err) {
|
|
655
|
+
throw rewriteCodeStepError(err, step.id, config.source, sourceURL);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function rewriteCodeStepError(err, stepId, source, sourceURL) {
|
|
659
|
+
if (!(err instanceof Error)) return new Error(String(err));
|
|
660
|
+
const WRAPPER_LINE_OFFSET = 2;
|
|
661
|
+
const sourceLines = source.split("\n");
|
|
662
|
+
const stack = err.stack || "";
|
|
663
|
+
const re = new RegExp(`${sourceURL.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}:(\\d+):(\\d+)`);
|
|
664
|
+
const match = stack.match(re);
|
|
665
|
+
if (!match) {
|
|
666
|
+
err.message = `Code step "${stepId}" failed: ${err.message}`;
|
|
667
|
+
return err;
|
|
668
|
+
}
|
|
669
|
+
const wrappedLine = parseInt(match[1], 10);
|
|
670
|
+
const col = parseInt(match[2], 10);
|
|
671
|
+
const userLine = wrappedLine - WRAPPER_LINE_OFFSET;
|
|
672
|
+
const lineContent = sourceLines[userLine - 1] ?? "";
|
|
673
|
+
const trimmed = lineContent.trim();
|
|
674
|
+
err.message = `Code step "${stepId}" failed at line ${userLine}:${col}
|
|
675
|
+
${trimmed}
|
|
676
|
+
${err.message}`;
|
|
677
|
+
err.stack = stack.replace(
|
|
678
|
+
new RegExp(`(${sourceURL.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}:)(\\d+)`, "g"),
|
|
679
|
+
(_m, prefix, l) => `${prefix}${parseInt(l, 10) - WRAPPER_LINE_OFFSET}`
|
|
680
|
+
);
|
|
681
|
+
return err;
|
|
624
682
|
}
|
|
625
683
|
async function executeCodeModule(stepId, modulePath, context, options) {
|
|
626
684
|
const rootDir = options.rootDir;
|
|
@@ -840,7 +898,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
840
898
|
if (flowStack.includes(resolvedKey)) {
|
|
841
899
|
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
842
900
|
}
|
|
843
|
-
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-
|
|
901
|
+
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-DFUFU2LM.js");
|
|
844
902
|
const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
|
|
845
903
|
const subContext = await executeFlow(
|
|
846
904
|
subFlow,
|
|
@@ -855,7 +913,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
855
913
|
return {
|
|
856
914
|
status: "success",
|
|
857
915
|
output: subContext.steps,
|
|
858
|
-
response: subContext
|
|
916
|
+
response: subContext.steps
|
|
859
917
|
};
|
|
860
918
|
}
|
|
861
919
|
async function executePaginateStep(step, context, api, permissions, allowedActionIds, options) {
|
|
@@ -951,13 +1009,14 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
951
1009
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
952
1010
|
try {
|
|
953
1011
|
if (attempt > 1) {
|
|
1012
|
+
const delay = computeRetryDelay(step.onError, attempt);
|
|
954
1013
|
options.onEvent?.({
|
|
955
1014
|
event: "step:retry",
|
|
956
1015
|
stepId: step.id,
|
|
957
1016
|
attempt,
|
|
958
|
-
maxRetries: step.onError.retries
|
|
1017
|
+
maxRetries: step.onError.retries,
|
|
1018
|
+
delayMs: delay
|
|
959
1019
|
});
|
|
960
|
-
const delay = step.onError?.retryDelayMs || 1e3;
|
|
961
1020
|
await sleep2(delay);
|
|
962
1021
|
}
|
|
963
1022
|
if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
|
|
@@ -1014,7 +1073,14 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1014
1073
|
throw new Error(`Unknown step type: ${step.type}`);
|
|
1015
1074
|
}
|
|
1016
1075
|
result.durationMs = Date.now() - startTime;
|
|
1017
|
-
if (attempt > 1)
|
|
1076
|
+
if (attempt > 1) {
|
|
1077
|
+
result.retries = attempt - 1;
|
|
1078
|
+
options.onEvent?.({
|
|
1079
|
+
event: "step:retry-success",
|
|
1080
|
+
stepId: step.id,
|
|
1081
|
+
retries: attempt - 1
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1018
1084
|
context.steps[step.id] = result;
|
|
1019
1085
|
return result;
|
|
1020
1086
|
} catch (err) {
|
|
@@ -1026,11 +1092,13 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1026
1092
|
}
|
|
1027
1093
|
const errorMessage = lastError?.message || "Unknown error";
|
|
1028
1094
|
const strategy = step.onError?.strategy || "fail";
|
|
1095
|
+
const retriesUsed = Math.max(0, maxAttempts - 1);
|
|
1029
1096
|
if (strategy === "continue") {
|
|
1030
1097
|
const result = {
|
|
1031
1098
|
status: "failed",
|
|
1032
1099
|
error: errorMessage,
|
|
1033
|
-
durationMs: Date.now() - startTime
|
|
1100
|
+
durationMs: Date.now() - startTime,
|
|
1101
|
+
retries: retriesUsed
|
|
1034
1102
|
};
|
|
1035
1103
|
context.steps[step.id] = result;
|
|
1036
1104
|
return result;
|
|
@@ -1039,7 +1107,8 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1039
1107
|
const result = {
|
|
1040
1108
|
status: "failed",
|
|
1041
1109
|
error: errorMessage,
|
|
1042
|
-
durationMs: Date.now() - startTime
|
|
1110
|
+
durationMs: Date.now() - startTime,
|
|
1111
|
+
retries: retriesUsed
|
|
1043
1112
|
};
|
|
1044
1113
|
context.steps[step.id] = result;
|
|
1045
1114
|
return result;
|
|
@@ -1614,7 +1683,7 @@ Every step MUST have \`id\`, \`name\`, and \`type\`. The \`type\` determines whi
|
|
|
1614
1683
|
for (const [name, fd] of Object.entries(FLOW_SCHEMA.stepCommonFields)) {
|
|
1615
1684
|
sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
|
|
1616
1685
|
}
|
|
1617
|
-
sections.push(`| \`onError\` | object | no | Error handling: \`{ "strategy": "${FLOW_SCHEMA.errorStrategies.join(" | ")}", "retries": 3, "retryDelayMs": 1000 }\` |`);
|
|
1686
|
+
sections.push(`| \`onError\` | object | no | Error handling: \`{ "strategy": "${FLOW_SCHEMA.errorStrategies.join(" | ")}", "retries": 3, "retryDelayMs": 1000, "backoff": "fixed \\| exponential \\| exponential-jitter", "maxDelayMs": 30000 }\` |`);
|
|
1618
1687
|
sections.push(`
|
|
1619
1688
|
## Step Types
|
|
1620
1689
|
|
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-Z45IBT5P.js";
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -2243,6 +2243,9 @@ function colorMethod(method) {
|
|
|
2243
2243
|
import pc7 from "picocolors";
|
|
2244
2244
|
|
|
2245
2245
|
// src/lib/flow-validator.ts
|
|
2246
|
+
import fs6 from "fs";
|
|
2247
|
+
import path6 from "path";
|
|
2248
|
+
import { spawnSync } from "child_process";
|
|
2246
2249
|
function validateFlowSchema(flow2) {
|
|
2247
2250
|
const errors = [];
|
|
2248
2251
|
if (!flow2 || typeof flow2 !== "object") {
|
|
@@ -2301,26 +2304,26 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
2301
2304
|
const validTypes = FLOW_SCHEMA.stepTypes.map((st) => st.type);
|
|
2302
2305
|
for (let i = 0; i < steps.length; i++) {
|
|
2303
2306
|
const step = steps[i];
|
|
2304
|
-
const
|
|
2307
|
+
const path8 = `${pathPrefix}[${i}]`;
|
|
2305
2308
|
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
2306
|
-
errors.push({ path:
|
|
2309
|
+
errors.push({ path: path8, message: "Step must be an object" });
|
|
2307
2310
|
continue;
|
|
2308
2311
|
}
|
|
2309
2312
|
const s = step;
|
|
2310
2313
|
if (!s.id || typeof s.id !== "string") {
|
|
2311
|
-
errors.push({ path: `${
|
|
2314
|
+
errors.push({ path: `${path8}.id`, message: 'Step must have a string "id"' });
|
|
2312
2315
|
}
|
|
2313
2316
|
if (!s.name || typeof s.name !== "string") {
|
|
2314
|
-
errors.push({ path: `${
|
|
2317
|
+
errors.push({ path: `${path8}.name`, message: 'Step must have a string "name"' });
|
|
2315
2318
|
}
|
|
2316
2319
|
if (!s.type || !validTypes.includes(s.type)) {
|
|
2317
|
-
errors.push({ path: `${
|
|
2320
|
+
errors.push({ path: `${path8}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
|
|
2318
2321
|
continue;
|
|
2319
2322
|
}
|
|
2320
2323
|
if (s.onError && typeof s.onError === "object") {
|
|
2321
2324
|
const oe = s.onError;
|
|
2322
2325
|
if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
|
|
2323
|
-
errors.push({ path: `${
|
|
2326
|
+
errors.push({ path: `${path8}.onError.strategy`, message: `Error strategy must be one of: ${FLOW_SCHEMA.errorStrategies.join(", ")}` });
|
|
2324
2327
|
}
|
|
2325
2328
|
}
|
|
2326
2329
|
const descriptor = getStepTypeDescriptor(s.type);
|
|
@@ -2330,14 +2333,14 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
2330
2333
|
if (!configObj || typeof configObj !== "object") {
|
|
2331
2334
|
const hint = detectFlatConfigHint(s, descriptor);
|
|
2332
2335
|
errors.push({
|
|
2333
|
-
path: `${
|
|
2336
|
+
path: `${path8}.${configKey}`,
|
|
2334
2337
|
message: `${capitalize(descriptor.type)} step must have a "${configKey}" config object${hint}`
|
|
2335
2338
|
});
|
|
2336
2339
|
continue;
|
|
2337
2340
|
}
|
|
2338
2341
|
const config2 = configObj;
|
|
2339
2342
|
for (const [fieldName, fd] of Object.entries(descriptor.fields)) {
|
|
2340
|
-
const fieldPath = `${
|
|
2343
|
+
const fieldPath = `${path8}.${configKey}.${fieldName}`;
|
|
2341
2344
|
const value = config2[fieldName];
|
|
2342
2345
|
if (fd.required && (value === void 0 || value === null || value === "")) {
|
|
2343
2346
|
errors.push({ path: fieldPath, message: `${capitalize(descriptor.type)} must have ${fd.type === "string" ? "a string" : fd.type === "array" ? "a" : "a"} "${fieldName}"` });
|
|
@@ -2373,18 +2376,24 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
2373
2376
|
const hasSource = typeof config2.source === "string" && config2.source.length > 0;
|
|
2374
2377
|
const hasModule = typeof config2.module === "string" && config2.module.length > 0;
|
|
2375
2378
|
if (!hasSource && !hasModule) {
|
|
2376
|
-
errors.push({ path: `${
|
|
2379
|
+
errors.push({ path: `${path8}.${configKey}`, message: 'Code step must define either "source" (inline JS) or "module" (path to .mjs file)' });
|
|
2377
2380
|
} else if (hasSource && hasModule) {
|
|
2378
|
-
errors.push({ path: `${
|
|
2381
|
+
errors.push({ path: `${path8}.${configKey}`, message: 'Code step cannot define both "source" and "module" \u2014 pick one' });
|
|
2379
2382
|
}
|
|
2380
2383
|
if (hasModule) {
|
|
2381
2384
|
const m = config2.module;
|
|
2382
2385
|
if (m.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(m)) {
|
|
2383
|
-
errors.push({ path: `${
|
|
2386
|
+
errors.push({ path: `${path8}.${configKey}.module`, message: "Code module path must be relative to the flow folder (no absolute paths)" });
|
|
2384
2387
|
} else if (m.split(/[\\/]/).includes("..")) {
|
|
2385
|
-
errors.push({ path: `${
|
|
2388
|
+
errors.push({ path: `${path8}.${configKey}.module`, message: 'Code module path must not escape the flow folder ("..")' });
|
|
2386
2389
|
} else if (!m.endsWith(".mjs")) {
|
|
2387
|
-
errors.push({ path: `${
|
|
2390
|
+
errors.push({ path: `${path8}.${configKey}.module`, message: "Code module must be a .mjs file" });
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (hasSource) {
|
|
2394
|
+
const syntaxError = checkCodeSourceSyntax(config2.source);
|
|
2395
|
+
if (syntaxError) {
|
|
2396
|
+
errors.push({ path: `${path8}.${configKey}.source`, message: `Syntax error in code step: ${syntaxError}` });
|
|
2388
2397
|
}
|
|
2389
2398
|
}
|
|
2390
2399
|
}
|
|
@@ -2401,6 +2410,17 @@ function detectFlatConfigHint(step, descriptor) {
|
|
|
2401
2410
|
function capitalize(s) {
|
|
2402
2411
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2403
2412
|
}
|
|
2413
|
+
var AsyncFunctionCtor = Object.getPrototypeOf(async function() {
|
|
2414
|
+
}).constructor;
|
|
2415
|
+
function checkCodeSourceSyntax(source) {
|
|
2416
|
+
try {
|
|
2417
|
+
new AsyncFunctionCtor("$", "require", source);
|
|
2418
|
+
return null;
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
if (err instanceof SyntaxError) return err.message;
|
|
2421
|
+
return err.message;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2404
2424
|
function validateStepIds(flow2) {
|
|
2405
2425
|
const errors = [];
|
|
2406
2426
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2408,16 +2428,16 @@ function validateStepIds(flow2) {
|
|
|
2408
2428
|
function collectIds(steps, pathPrefix) {
|
|
2409
2429
|
for (let i = 0; i < steps.length; i++) {
|
|
2410
2430
|
const step = steps[i];
|
|
2411
|
-
const
|
|
2431
|
+
const path8 = `${pathPrefix}[${i}]`;
|
|
2412
2432
|
if (seen.has(step.id)) {
|
|
2413
|
-
errors.push({ path: `${
|
|
2433
|
+
errors.push({ path: `${path8}.id`, message: `Duplicate step ID: "${step.id}"` });
|
|
2414
2434
|
} else {
|
|
2415
2435
|
seen.add(step.id);
|
|
2416
2436
|
}
|
|
2417
2437
|
for (const { configKey, fieldName } of nestedKeys) {
|
|
2418
2438
|
const config2 = step[configKey];
|
|
2419
2439
|
if (config2 && Array.isArray(config2[fieldName])) {
|
|
2420
|
-
collectIds(config2[fieldName], `${
|
|
2440
|
+
collectIds(config2[fieldName], `${path8}.${configKey}.${fieldName}`);
|
|
2421
2441
|
}
|
|
2422
2442
|
}
|
|
2423
2443
|
}
|
|
@@ -2465,7 +2485,7 @@ function validateSelectorReferences(flow2) {
|
|
|
2465
2485
|
}
|
|
2466
2486
|
return selectors;
|
|
2467
2487
|
}
|
|
2468
|
-
function checkSelectors(selectors,
|
|
2488
|
+
function checkSelectors(selectors, path8, precedingStepIds) {
|
|
2469
2489
|
for (const selector of selectors) {
|
|
2470
2490
|
const parts = selector.split(".");
|
|
2471
2491
|
if (parts.length < 3) continue;
|
|
@@ -2473,37 +2493,42 @@ function validateSelectorReferences(flow2) {
|
|
|
2473
2493
|
if (root === "input") {
|
|
2474
2494
|
const inputName = parts[2];
|
|
2475
2495
|
if (!inputNames.has(inputName)) {
|
|
2476
|
-
errors.push({ path:
|
|
2496
|
+
errors.push({ path: path8, message: `Selector "${selector}" references undefined input "${inputName}"` });
|
|
2477
2497
|
}
|
|
2478
2498
|
} else if (root === "steps") {
|
|
2479
|
-
const stepId = parts[2];
|
|
2499
|
+
const stepId = parts[2].replace(/[\[\]]/g, "").split(/[\[\]]/)[0];
|
|
2480
2500
|
if (!allStepIds.has(stepId)) {
|
|
2481
|
-
errors.push({ path:
|
|
2501
|
+
errors.push({ path: path8, message: `Selector "${selector}" references undefined step "${stepId}"` });
|
|
2502
|
+
} else if (precedingStepIds && !precedingStepIds.has(stepId)) {
|
|
2503
|
+
errors.push({
|
|
2504
|
+
path: path8,
|
|
2505
|
+
message: `Selector "${selector}" references step "${stepId}" which is declared after the current step. Steps execute in declaration order, so this will always resolve to undefined at runtime \u2014 move the dependency earlier in the steps array.`
|
|
2506
|
+
});
|
|
2482
2507
|
}
|
|
2483
2508
|
}
|
|
2484
2509
|
}
|
|
2485
2510
|
}
|
|
2486
2511
|
const EXPRESSION_FIELDS = /* @__PURE__ */ new Set(["condition.expression", "while.condition"]);
|
|
2487
|
-
function checkOperatorsInSelectorField(value,
|
|
2512
|
+
function checkOperatorsInSelectorField(value, path8) {
|
|
2488
2513
|
if (typeof value === "string" && value.startsWith("$.")) {
|
|
2489
2514
|
if (value.includes("||")) {
|
|
2490
|
-
errors.push({ path:
|
|
2515
|
+
errors.push({ path: path8, message: `Selector "${value}" contains unsupported operator "||". Selectors in data fields use dot-path resolution, not JS evaluation. Use the "default" field on the input definition instead, or use a "code" step for complex expressions.` });
|
|
2491
2516
|
} else if (value.includes("&&")) {
|
|
2492
|
-
errors.push({ path:
|
|
2517
|
+
errors.push({ path: path8, message: `Selector "${value}" contains unsupported operator "&&". Selectors in data fields use dot-path resolution, not JS evaluation. Use a "condition" step or "code" step for complex expressions.` });
|
|
2493
2518
|
}
|
|
2494
2519
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2495
2520
|
for (const [k, v] of Object.entries(value)) {
|
|
2496
|
-
checkOperatorsInSelectorField(v, `${
|
|
2521
|
+
checkOperatorsInSelectorField(v, `${path8}.${k}`);
|
|
2497
2522
|
}
|
|
2498
2523
|
} else if (Array.isArray(value)) {
|
|
2499
2524
|
for (let i = 0; i < value.length; i++) {
|
|
2500
|
-
checkOperatorsInSelectorField(value[i], `${
|
|
2525
|
+
checkOperatorsInSelectorField(value[i], `${path8}[${i}]`);
|
|
2501
2526
|
}
|
|
2502
2527
|
}
|
|
2503
2528
|
}
|
|
2504
|
-
function checkStep(step, pathPrefix) {
|
|
2505
|
-
if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if
|
|
2506
|
-
if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless
|
|
2529
|
+
function checkStep(step, pathPrefix, preceding2) {
|
|
2530
|
+
if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`, preceding2);
|
|
2531
|
+
if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`, preceding2);
|
|
2507
2532
|
const descriptor = getStepTypeDescriptor(step.type);
|
|
2508
2533
|
if (descriptor) {
|
|
2509
2534
|
const config2 = step[descriptor.configKey];
|
|
@@ -2515,41 +2540,95 @@ function validateSelectorReferences(flow2) {
|
|
|
2515
2540
|
if (value !== void 0) {
|
|
2516
2541
|
const fieldKey = `${descriptor.configKey}.${fieldName}`;
|
|
2517
2542
|
const fieldPath = `${pathPrefix}.${fieldKey}`;
|
|
2518
|
-
checkSelectors(extractSelectors(value), fieldPath);
|
|
2543
|
+
checkSelectors(extractSelectors(value), fieldPath, preceding2);
|
|
2519
2544
|
if (!EXPRESSION_FIELDS.has(fieldKey)) {
|
|
2520
2545
|
checkOperatorsInSelectorField(value, fieldPath);
|
|
2521
2546
|
}
|
|
2522
2547
|
}
|
|
2523
2548
|
}
|
|
2549
|
+
} else {
|
|
2550
|
+
const c = config2;
|
|
2551
|
+
const codeSource = step.type === "code" ? c.source : void 0;
|
|
2552
|
+
const transformExpr = step.type === "transform" ? c.expression : void 0;
|
|
2553
|
+
const text4 = codeSource ?? transformExpr;
|
|
2554
|
+
if (typeof text4 === "string") {
|
|
2555
|
+
const fieldName = step.type === "code" ? "source" : "expression";
|
|
2556
|
+
checkSelectors(extractSelectors(text4), `${pathPrefix}.${descriptor.configKey}.${fieldName}`, preceding2);
|
|
2557
|
+
}
|
|
2524
2558
|
}
|
|
2525
2559
|
}
|
|
2526
2560
|
for (const { configKey, fieldName } of nestedKeys) {
|
|
2527
2561
|
if (configKey === descriptor.configKey) {
|
|
2528
2562
|
const c = step[configKey];
|
|
2529
2563
|
if (c && Array.isArray(c[fieldName])) {
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2564
|
+
const childPreceding = new Set(preceding2);
|
|
2565
|
+
c[fieldName].forEach((s, i) => {
|
|
2566
|
+
checkStep(s, `${pathPrefix}.${configKey}.${fieldName}[${i}]`, childPreceding);
|
|
2567
|
+
childPreceding.add(s.id);
|
|
2568
|
+
});
|
|
2533
2569
|
}
|
|
2534
2570
|
}
|
|
2535
2571
|
}
|
|
2536
2572
|
}
|
|
2537
2573
|
}
|
|
2538
|
-
|
|
2574
|
+
const preceding = /* @__PURE__ */ new Set();
|
|
2575
|
+
flow2.steps.forEach((step, i) => {
|
|
2576
|
+
checkStep(step, `steps[${i}]`, preceding);
|
|
2577
|
+
preceding.add(step.id);
|
|
2578
|
+
});
|
|
2539
2579
|
return errors;
|
|
2540
2580
|
}
|
|
2541
|
-
function validateFlow(flow2) {
|
|
2581
|
+
function validateFlow(flow2, rootDir) {
|
|
2542
2582
|
const schemaErrors = validateFlowSchema(flow2);
|
|
2543
2583
|
if (schemaErrors.length > 0) return schemaErrors;
|
|
2544
2584
|
const f = flow2;
|
|
2545
2585
|
return [
|
|
2546
2586
|
...validateStepIds(f),
|
|
2547
|
-
...validateSelectorReferences(f)
|
|
2587
|
+
...validateSelectorReferences(f),
|
|
2588
|
+
...rootDir ? validateCodeModules(f, rootDir) : []
|
|
2548
2589
|
];
|
|
2549
2590
|
}
|
|
2591
|
+
function validateCodeModules(flow2, rootDir) {
|
|
2592
|
+
const errors = [];
|
|
2593
|
+
const nestedKeys = getNestedStepsKeys();
|
|
2594
|
+
function walk(steps, pathPrefix) {
|
|
2595
|
+
for (let i = 0; i < steps.length; i++) {
|
|
2596
|
+
const step = steps[i];
|
|
2597
|
+
const stepPath = `${pathPrefix}[${i}]`;
|
|
2598
|
+
if (step.type === "code" && step.code?.module) {
|
|
2599
|
+
const m = step.code.module;
|
|
2600
|
+
const abs = path6.resolve(rootDir, m);
|
|
2601
|
+
if (!fs6.existsSync(abs)) {
|
|
2602
|
+
errors.push({
|
|
2603
|
+
path: `${stepPath}.code.module`,
|
|
2604
|
+
message: `Code module "${m}" not found at ${abs}`
|
|
2605
|
+
});
|
|
2606
|
+
} else {
|
|
2607
|
+
const res = spawnSync(process.execPath, ["--check", abs], { encoding: "utf-8" });
|
|
2608
|
+
if (res.status !== 0) {
|
|
2609
|
+
const msg = (res.stderr || "").split("\n").find((l) => l.includes("SyntaxError") || l.includes("Error")) || res.stderr || "Syntax check failed";
|
|
2610
|
+
errors.push({
|
|
2611
|
+
path: `${stepPath}.code.module`,
|
|
2612
|
+
message: `Syntax error in code module "${m}": ${msg.trim()}`
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
for (const { configKey, fieldName } of nestedKeys) {
|
|
2618
|
+
const config2 = step[configKey];
|
|
2619
|
+
if (config2 && Array.isArray(config2[fieldName])) {
|
|
2620
|
+
walk(config2[fieldName], `${stepPath}.${configKey}.${fieldName}`);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
walk(flow2.steps, "steps");
|
|
2626
|
+
return errors;
|
|
2627
|
+
}
|
|
2550
2628
|
|
|
2551
2629
|
// src/commands/flow.ts
|
|
2552
|
-
import
|
|
2630
|
+
import fs7 from "fs";
|
|
2631
|
+
import path7 from "path";
|
|
2553
2632
|
function getConfig2() {
|
|
2554
2633
|
const apiKey = getApiKey();
|
|
2555
2634
|
if (!apiKey) {
|
|
@@ -2607,7 +2686,7 @@ async function flowCreateCommand(key, options) {
|
|
|
2607
2686
|
if (raw.startsWith("@")) {
|
|
2608
2687
|
const filePath = raw.slice(1);
|
|
2609
2688
|
try {
|
|
2610
|
-
raw =
|
|
2689
|
+
raw = fs7.readFileSync(filePath, "utf-8");
|
|
2611
2690
|
} catch (err) {
|
|
2612
2691
|
error(`Cannot read file "${filePath}": ${err.message}`);
|
|
2613
2692
|
}
|
|
@@ -2672,6 +2751,15 @@ async function flowExecuteCommand(keyOrPath, options) {
|
|
|
2672
2751
|
return;
|
|
2673
2752
|
}
|
|
2674
2753
|
spinner5.stop(`Workflow: ${flow2.name} (${flow2.steps.length} steps)`);
|
|
2754
|
+
const preflightErrors = validateFlow(flow2, rootDir);
|
|
2755
|
+
if (preflightErrors.length > 0) {
|
|
2756
|
+
if (isAgentMode()) {
|
|
2757
|
+
json({ error: "Validation failed", errors: preflightErrors });
|
|
2758
|
+
process.exit(1);
|
|
2759
|
+
}
|
|
2760
|
+
error(`Validation failed:
|
|
2761
|
+
${preflightErrors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}`);
|
|
2762
|
+
}
|
|
2675
2763
|
if (flowFilePath.endsWith(".flow.json")) {
|
|
2676
2764
|
const msg = `Workflow "${flow2.key}" uses the deprecated single-file layout. Migrate to .one/flows/${flow2.key}/flow.json (see: one guide flows).`;
|
|
2677
2765
|
if (isAgentMode()) {
|
|
@@ -2812,15 +2900,23 @@ async function flowValidateCommand(keyOrPath) {
|
|
|
2812
2900
|
const spinner5 = createSpinner();
|
|
2813
2901
|
spinner5.start(`Validating "${keyOrPath}"...`);
|
|
2814
2902
|
let flowData;
|
|
2903
|
+
let rootDir;
|
|
2815
2904
|
try {
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2905
|
+
try {
|
|
2906
|
+
const loaded = loadFlowWithMeta(keyOrPath);
|
|
2907
|
+
flowData = loaded.flow;
|
|
2908
|
+
rootDir = loaded.rootDir;
|
|
2909
|
+
} catch {
|
|
2910
|
+
const flowPath = resolveFlowPath(keyOrPath);
|
|
2911
|
+
const content = fs7.readFileSync(flowPath, "utf-8");
|
|
2912
|
+
flowData = JSON.parse(content);
|
|
2913
|
+
rootDir = path7.dirname(flowPath);
|
|
2914
|
+
}
|
|
2819
2915
|
} catch (err) {
|
|
2820
2916
|
spinner5.stop("Validation failed");
|
|
2821
2917
|
error(`Could not read workflow: ${err instanceof Error ? err.message : String(err)}`);
|
|
2822
2918
|
}
|
|
2823
|
-
const errors = validateFlow(flowData);
|
|
2919
|
+
const errors = validateFlow(flowData, rootDir);
|
|
2824
2920
|
if (errors.length > 0) {
|
|
2825
2921
|
spinner5.stop("Validation failed");
|
|
2826
2922
|
if (isAgentMode()) {
|
package/package.json
CHANGED
|
@@ -95,6 +95,21 @@ process.stdout.write(JSON.stringify(items.filter(i => i.active)));
|
|
|
95
95
|
|
|
96
96
|
The module runs as a child `node` process: the flow context `$` is piped to stdin as JSON, and stdout is parsed as JSON and used as the step's output. Modules have full Node APIs available (unlike inline `code.source`, which is sandboxed). Use `code.module` for anything non-trivial; keep `code.source` for one-liners.
|
|
97
97
|
|
|
98
|
+
#### Inline `code.source` sandbox
|
|
99
|
+
|
|
100
|
+
Inline `code.source` runs inside an async function with a restricted `require`. Only the following Node built-ins are importable:
|
|
101
|
+
|
|
102
|
+
- `node:buffer`
|
|
103
|
+
- `node:crypto`
|
|
104
|
+
- `node:url`
|
|
105
|
+
- `node:path`
|
|
106
|
+
|
|
107
|
+
Everything else — `fs`, `http`, `https`, `net`, `child_process`, `process`, `os`, `cluster`, `dgram`, `tls`, `vm`, `worker_threads` — is **blocked** and will throw `Module "<name>" is blocked in code steps`. The runtime also does not expose `process`, `__dirname`, `__filename`, `setTimeout`, or `fetch`.
|
|
108
|
+
|
|
109
|
+
If you need any of those (filesystem reads, network calls, timers, etc.), use a `code.module` step instead — modules run as a real child `node` process and have the full Node API surface.
|
|
110
|
+
|
|
111
|
+
When an inline `code.source` step throws at runtime, the error message reports the user-relative line and column plus the offending line of source — e.g. `Code step "blowup" failed at line 3:34\n const c = $.steps.mk.output.data.score;\n Cannot read properties of null (reading 'score')`. No need to bisect the step manually.
|
|
112
|
+
|
|
98
113
|
Whatever JSON a module writes to stdout becomes both `$.steps.<id>.output` and `$.steps.<id>.response` (aliases). Downstream steps can reference either.
|
|
99
114
|
|
|
100
115
|
### Migrating a legacy single-file flow
|
|
@@ -130,6 +145,8 @@ Two rules: (1) prepend the stdin-read line, (2) replace `return X` with `process
|
|
|
130
145
|
one --agent flow validate <key>
|
|
131
146
|
```
|
|
132
147
|
|
|
148
|
+
`flow validate` parses every inline `code.source` and runs `node --check` on every `code.module` file, so syntax errors (brace/paren mismatches, duplicate `let`, etc.) surface here instead of after upstream steps have already run. It also extracts `$.steps.X` and `$.input.X` references from inside `code.source` and `transform.expression` and reports any reference to an undefined step/input or to a step declared **after** the current one (forward references resolve to `undefined` at runtime — silent data loss). The same checks run automatically at the start of `flow execute` so a broken step in position 15 fails the run immediately rather than 15 minutes in.
|
|
149
|
+
|
|
133
150
|
### Step 6: Execute
|
|
134
151
|
|
|
135
152
|
```bash
|
|
@@ -187,6 +204,16 @@ Connection inputs with a `connection` field auto-resolve if the user has exactly
|
|
|
187
204
|
|
|
188
205
|
A pure `$.xxx` value resolves to the raw type. A string containing `{{$.xxx}}` does string interpolation.
|
|
189
206
|
|
|
207
|
+
**Passing objects and arrays:** `{{ }}` interpolation always produces a string — if the resolved value is an object or array it will be JSON-stringified and the engine will log a warning. To pass an object/array as a native value to the next step, use a **direct selector without `{{ }}`**:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
// ✗ Wrong — becomes a JSON string, triggers a runtime warning
|
|
211
|
+
"files": "{{$.steps.extract.output.allFiles}}"
|
|
212
|
+
|
|
213
|
+
// ✓ Right — passes the array as an array
|
|
214
|
+
"files": "$.steps.extract.output.allFiles"
|
|
215
|
+
```
|
|
216
|
+
|
|
190
217
|
### Selectors vs expressions
|
|
191
218
|
|
|
192
219
|
Selectors in data fields (`data`, `queryParams`, `pathVars`, `connectionKey`) are **dot-path lookups only** — they do not support JavaScript operators like `||` or `&&`. For default values, use the `default` field on the input definition:
|
|
@@ -343,6 +370,14 @@ After a parallel step, access each substep's output by its `id`: `$.steps.fetchE
|
|
|
343
370
|
}
|
|
344
371
|
```
|
|
345
372
|
|
|
373
|
+
A sub-flow step exposes the sub-flow's **step results map** at both `.output` and `.response` (they are aliases — pick whichever reads better). Access a specific sub-step's data with:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
$.steps.<parent>.output.<subStepId>.output.<field>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
e.g. if sub-flow `enrich-customer` has a step `load` that returns `{ TEAM: "acme" }`, the caller reads it as `$.steps.enrich.output.load.output.TEAM`. There is no longer any `.response.<subStepId>` vs `.output.<subStepId>` ambiguity.
|
|
380
|
+
|
|
346
381
|
### `paginate` — Auto-collect paginated results
|
|
347
382
|
|
|
348
383
|
```json
|
|
@@ -369,6 +404,26 @@ After a parallel step, access each substep's output by its `id`: `$.steps.fetchE
|
|
|
369
404
|
}
|
|
370
405
|
```
|
|
371
406
|
|
|
407
|
+
**Safe interpolation.** Plain `{{$.input.x}}` does string substitution and is **unsafe** for bash — values containing quotes, `$`, backticks, `&`, etc. will break the command (or worse). Use the `q` helper to POSIX-shell-quote the value:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{ "command": "echo {{q $.input.companyName}} | tr '[:upper:]' '[:lower:]'" }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`{{q $.input.companyName}}` resolves `O'Reilly Media & Co` to `'O'\''Reilly Media & Co'` — a single argv token bash will parse cleanly. Use `{{q ...}}` for **every** interpolation of user-controlled data into a bash command.
|
|
414
|
+
|
|
415
|
+
Alternatively, pass values as environment variables (also shell-safe) and reference them with `$VAR`:
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{
|
|
419
|
+
"type": "bash",
|
|
420
|
+
"bash": {
|
|
421
|
+
"env": { "COMPANY": "$.input.companyName" },
|
|
422
|
+
"command": "echo \"$COMPANY\" | tr '[:upper:]' '[:lower:]'"
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
372
427
|
## Error Handling
|
|
373
428
|
|
|
374
429
|
```json
|
|
@@ -377,6 +432,30 @@ After a parallel step, access each substep's output by its `id`: `$.steps.fetchE
|
|
|
377
432
|
|
|
378
433
|
Strategies: `fail` (default), `continue`, `retry`, `fallback`.
|
|
379
434
|
|
|
435
|
+
**Retry backoff.** By default each retry waits exactly `retryDelayMs`. For rate-limited APIs add `"backoff": "exponential"` (or `"exponential-jitter"`) and an optional `"maxDelayMs"` cap (defaults to 30000):
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
{
|
|
439
|
+
"onError": {
|
|
440
|
+
"strategy": "retry",
|
|
441
|
+
"retries": 4,
|
|
442
|
+
"retryDelayMs": 1000,
|
|
443
|
+
"backoff": "exponential-jitter",
|
|
444
|
+
"maxDelayMs": 10000
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`exponential` waits `retryDelayMs * 2^(retryIndex)` (1s, 2s, 4s, 8s…) capped at `maxDelayMs`. `exponential-jitter` multiplies each wait by a random factor in [0.5, 1.0) so concurrent retries spread out.
|
|
450
|
+
|
|
451
|
+
**Inspecting retry outcomes.** Every retried step exposes how it ended on its `StepResult`:
|
|
452
|
+
|
|
453
|
+
- `$.steps.<id>.status` — `"success"` or `"failed"`
|
|
454
|
+
- `$.steps.<id>.retries` — number of retries actually performed (0 if first attempt succeeded)
|
|
455
|
+
- `$.steps.<id>.error` — last error message (only set when `status === "failed"` under `continue`/`fallback` strategies)
|
|
456
|
+
|
|
457
|
+
A successful-after-retry step also emits a `step:retry-success` event with the retry count, so you can distinguish a clean first-attempt success from a recovered one in logs.
|
|
458
|
+
|
|
380
459
|
Conditional execution: `"if": "$.steps.find.response.data.length > 0"`
|
|
381
460
|
|
|
382
461
|
## AI-Augmented Patterns
|