blokctl 0.7.0 → 1.0.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/dist/commands/create/project.js +7 -2
- package/dist/commands/create/utils/Examples.d.ts +3 -3
- package/dist/commands/create/utils/Examples.js +68 -9
- package/dist/commands/generate/WorkflowGenerator.js +4 -4
- package/dist/commands/generate/WorkflowGenerator.test.js +2 -2
- package/dist/commands/generate/prompts/create-fn-node.system.js +10 -17
- package/dist/commands/generate/prompts/create-workflow.system.js +114 -375
- package/dist/commands/generate/validators/WorkflowValidator.js +80 -5
- package/dist/commands/generate/validators/WorkflowValidator.test.js +66 -0
- package/package.json +2 -2
|
@@ -29,15 +29,21 @@ export function validateWorkflow(jsonString) {
|
|
|
29
29
|
warnings: [],
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
const v2 = isV2Workflow(workflow.steps);
|
|
33
|
+
validateTopLevel(workflow, v2, errors, warnings);
|
|
33
34
|
if (workflow.trigger && typeof workflow.trigger === "object") {
|
|
34
35
|
validateTrigger(workflow.trigger, errors, warnings);
|
|
35
36
|
}
|
|
36
37
|
const stepNames = new Set();
|
|
37
38
|
if (Array.isArray(workflow.steps)) {
|
|
38
|
-
|
|
39
|
+
if (v2) {
|
|
40
|
+
validateV2Steps(workflow.steps, stepNames, errors, warnings);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
validateSteps(workflow.steps, stepNames, errors, warnings);
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
|
-
if (workflow.nodes && typeof workflow.nodes === "object") {
|
|
46
|
+
if (!v2 && workflow.nodes && typeof workflow.nodes === "object") {
|
|
41
47
|
validateNodes(workflow.nodes, stepNames, errors, warnings);
|
|
42
48
|
}
|
|
43
49
|
return {
|
|
@@ -46,7 +52,17 @@ export function validateWorkflow(jsonString) {
|
|
|
46
52
|
warnings,
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
|
-
function
|
|
55
|
+
function isV2Workflow(steps) {
|
|
56
|
+
if (!Array.isArray(steps))
|
|
57
|
+
return false;
|
|
58
|
+
return steps.some((s) => {
|
|
59
|
+
if (!s || typeof s !== "object")
|
|
60
|
+
return false;
|
|
61
|
+
const step = s;
|
|
62
|
+
return step.use !== undefined || step.branch !== undefined || (step.id !== undefined && step.name === undefined);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function validateTopLevel(workflow, v2, errors, warnings) {
|
|
50
66
|
if (!workflow.name || typeof workflow.name !== "string") {
|
|
51
67
|
errors.push('Missing or invalid "name" field (must be a non-empty string)');
|
|
52
68
|
}
|
|
@@ -71,7 +87,7 @@ function validateTopLevel(workflow, errors, warnings) {
|
|
|
71
87
|
else if (workflow.steps.length === 0) {
|
|
72
88
|
errors.push('"steps" array must not be empty');
|
|
73
89
|
}
|
|
74
|
-
if (!workflow.nodes || typeof workflow.nodes !== "object") {
|
|
90
|
+
if (!v2 && (!workflow.nodes || typeof workflow.nodes !== "object")) {
|
|
75
91
|
errors.push('Missing or invalid "nodes" field (must be an object)');
|
|
76
92
|
}
|
|
77
93
|
}
|
|
@@ -205,6 +221,65 @@ function validateSteps(steps, stepNames, errors, warnings) {
|
|
|
205
221
|
stepNames.add(step.name);
|
|
206
222
|
}
|
|
207
223
|
}
|
|
224
|
+
function validateV2Steps(steps, ids, errors, warnings) {
|
|
225
|
+
for (let i = 0; i < steps.length; i++) {
|
|
226
|
+
const step = steps[i];
|
|
227
|
+
if (!step || typeof step !== "object") {
|
|
228
|
+
errors.push(`Step at index ${i} must be an object`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!step.id || typeof step.id !== "string") {
|
|
232
|
+
errors.push(`Step at index ${i} is missing required "id" field`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ids.has(step.id)) {
|
|
236
|
+
errors.push(`Duplicate step id "${step.id}" — step ids are flat per workflow (including branch arms)`);
|
|
237
|
+
}
|
|
238
|
+
ids.add(step.id);
|
|
239
|
+
if (step.branch !== undefined) {
|
|
240
|
+
const branch = step.branch;
|
|
241
|
+
if (!branch || typeof branch !== "object") {
|
|
242
|
+
errors.push(`Step "${step.id}" has an invalid "branch" (must be an object)`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!branch.when || typeof branch.when !== "string") {
|
|
246
|
+
errors.push(`Branch step "${step.id}" is missing a "when" condition string`);
|
|
247
|
+
}
|
|
248
|
+
else if (branch.when.startsWith("js/") || branch.when.startsWith("$.")) {
|
|
249
|
+
errors.push(`Branch step "${step.id}" "when" must be a raw \`ctx.*\` expression (e.g. "ctx.state.x > 10") — never "js/..." or "$...", which throw at runtime`);
|
|
250
|
+
}
|
|
251
|
+
if (!Array.isArray(branch.then) || branch.then.length === 0) {
|
|
252
|
+
errors.push(`Branch step "${step.id}" must have a non-empty "then" array`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
validateV2Steps(branch.then, ids, errors, warnings);
|
|
256
|
+
}
|
|
257
|
+
if (branch.else !== undefined) {
|
|
258
|
+
if (!Array.isArray(branch.else)) {
|
|
259
|
+
errors.push(`Branch step "${step.id}" "else" must be an array when present`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
validateV2Steps(branch.else, ids, errors, warnings);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if ((!step.use || typeof step.use !== "string") && typeof step.subworkflow !== "string") {
|
|
268
|
+
errors.push(`Step "${step.id}" is missing required "use" field (the node to run)`);
|
|
269
|
+
}
|
|
270
|
+
if (step.type !== undefined) {
|
|
271
|
+
if (typeof step.type !== "string") {
|
|
272
|
+
errors.push(`Step "${step.id}" "type" must be a string`);
|
|
273
|
+
}
|
|
274
|
+
else if (!VALID_STEP_TYPES.includes(step.type) && !step.type.startsWith("runtime.")) {
|
|
275
|
+
warnings.push(`Step "${step.id}" has unusual type "${step.type}". Common types: ${VALID_STEP_TYPES.join(", ")}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (step.as !== undefined && step.spread !== undefined) {
|
|
279
|
+
errors.push(`Step "${step.id}" sets both "as" and "spread" — they are mutually exclusive`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
208
283
|
function validateNodes(nodes, stepNames, errors, warnings) {
|
|
209
284
|
const allReferencedStepNames = new Set(stepNames);
|
|
210
285
|
for (const [nodeName, nodeConfig] of Object.entries(nodes)) {
|
|
@@ -644,4 +644,70 @@ describe("WorkflowValidator", () => {
|
|
|
644
644
|
expect(result.valid).toBe(true);
|
|
645
645
|
});
|
|
646
646
|
});
|
|
647
|
+
describe("v2 workflows (inline inputs, no nodes map)", () => {
|
|
648
|
+
const base = {
|
|
649
|
+
name: "V2 Flow",
|
|
650
|
+
description: "A v2 workflow",
|
|
651
|
+
version: "1.0.0",
|
|
652
|
+
trigger: { http: { method: "GET", path: "/v2" } },
|
|
653
|
+
};
|
|
654
|
+
it("accepts a v2 workflow with id/use/inputs and no nodes map", () => {
|
|
655
|
+
const result = validateWorkflow(JSON.stringify({
|
|
656
|
+
...base,
|
|
657
|
+
steps: [
|
|
658
|
+
{ id: "fetch", use: "@blokjs/api-call", inputs: { url: "https://x" } },
|
|
659
|
+
{ id: "respond", use: "@blokjs/respond", inputs: { body: "$.state.fetch" }, ephemeral: true },
|
|
660
|
+
],
|
|
661
|
+
}));
|
|
662
|
+
expect(result.valid).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
it("accepts a v2 branch step and validates its arm ids", () => {
|
|
665
|
+
const result = validateWorkflow(JSON.stringify({
|
|
666
|
+
...base,
|
|
667
|
+
steps: [
|
|
668
|
+
{
|
|
669
|
+
id: "route",
|
|
670
|
+
branch: {
|
|
671
|
+
when: "ctx.req.method === 'POST'",
|
|
672
|
+
then: [{ id: "create", use: "@blokjs/api-call", inputs: {} }],
|
|
673
|
+
else: [{ id: "read", use: "@blokjs/api-call", inputs: {} }],
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
}));
|
|
678
|
+
expect(result.valid).toBe(true);
|
|
679
|
+
});
|
|
680
|
+
it("errors on a v2 step missing use", () => {
|
|
681
|
+
const result = validateWorkflow(JSON.stringify({ ...base, steps: [{ id: "x", inputs: {} }] }));
|
|
682
|
+
expect(result.valid).toBe(false);
|
|
683
|
+
expect(result.errors.some((e) => e.includes('missing required "use"'))).toBe(true);
|
|
684
|
+
});
|
|
685
|
+
it("errors on a duplicate id across a branch arm", () => {
|
|
686
|
+
const result = validateWorkflow(JSON.stringify({
|
|
687
|
+
...base,
|
|
688
|
+
steps: [
|
|
689
|
+
{ id: "dup", use: "@blokjs/api-call", inputs: {} },
|
|
690
|
+
{
|
|
691
|
+
id: "b",
|
|
692
|
+
branch: { when: "ctx.state.dup.ok", then: [{ id: "dup", use: "@blokjs/respond", inputs: {} }] },
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
}));
|
|
696
|
+
expect(result.valid).toBe(false);
|
|
697
|
+
expect(result.errors.some((e) => e.includes("Duplicate step id"))).toBe(true);
|
|
698
|
+
});
|
|
699
|
+
it("errors on a branch when prefixed with js/ or $.", () => {
|
|
700
|
+
const result = validateWorkflow(JSON.stringify({
|
|
701
|
+
...base,
|
|
702
|
+
steps: [{ id: "b", branch: { when: "js/ctx.state.x", then: [{ id: "t", use: "n", inputs: {} }] } }],
|
|
703
|
+
}));
|
|
704
|
+
expect(result.valid).toBe(false);
|
|
705
|
+
expect(result.errors.some((e) => e.includes("raw"))).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
it("errors when a v2 step sets both as and spread", () => {
|
|
708
|
+
const result = validateWorkflow(JSON.stringify({ ...base, steps: [{ id: "x", use: "n", as: "y", spread: true, inputs: {} }] }));
|
|
709
|
+
expect(result.valid).toBe(false);
|
|
710
|
+
expect(result.errors.some((e) => e.includes("mutually exclusive"))).toBe(true);
|
|
711
|
+
});
|
|
712
|
+
});
|
|
647
713
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blokctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"author": "Deskree Technologies Inc.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "cli for blok",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"keywords": ["blokctl", "cli", "blok", "blok"],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@ai-sdk/openai": "^1.3.22",
|
|
33
|
-
"@blokjs/runner": "^0.
|
|
33
|
+
"@blokjs/runner": "^1.0.0",
|
|
34
34
|
"@clack/prompts": "^1.0.0",
|
|
35
35
|
"ai": "^4.3.16",
|
|
36
36
|
"better-sqlite3": "^12.6.2",
|