@bpmnkit/cli 0.0.16 → 0.0.18

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 CHANGED
@@ -109,6 +109,8 @@ casen instances list --state active
109
109
  | [`@bpmnkit/cli-sdk`](https://www.npmjs.com/package/@bpmnkit/cli-sdk) | Plugin authoring SDK for the casen CLI |
110
110
  | [`@bpmnkit/create-casen-plugin`](https://www.npmjs.com/package/@bpmnkit/create-casen-plugin) | Scaffold a new casen CLI plugin in seconds |
111
111
  | [`@bpmnkit/casen-report`](https://www.npmjs.com/package/@bpmnkit/casen-report) | HTML reports from Camunda 8 incident and SLA data |
112
+ | [`@bpmnkit/casen-worker-http`](https://www.npmjs.com/package/@bpmnkit/casen-worker-http) | Example HTTP worker plugin — completes jobs with live JSONPlaceholder API data |
113
+ | [`@bpmnkit/casen-worker-ai`](https://www.npmjs.com/package/@bpmnkit/casen-worker-ai) | AI task worker — classify, summarize, extract, and decide using Claude |
112
114
 
113
115
  ## License
114
116
 
@@ -4,10 +4,13 @@ import { askGroup } from "./ask.js";
4
4
  import { getDmnReqsXmlCmd, getDmnXmlCmd, getStartFormCmd, getUserTaskFormCmd, getXmlCmd, renderBpmnCmd, } from "./bpmn.js";
5
5
  import { completionGroup } from "./completion.js";
6
6
  import { connectorGroup } from "./connector.js";
7
+ import { lintGroup } from "./lint.js";
7
8
  import { pluginGroup } from "./plugin.js";
8
9
  import { profileGroup } from "./profile.js";
9
10
  import { computeRelations } from "./relations.js";
10
11
  import { settingsGroup } from "./settings.js";
12
+ import { storyGroup } from "./story.js";
13
+ import { testGroup } from "./test.js";
11
14
  import { workerCmd } from "./worker.js";
12
15
  // Inject custom commands into generated groups without modifying generated files.
13
16
  // Also remove the broken generated get-x-m-l commands (return text/xml, not JSON)
@@ -30,23 +33,36 @@ const customisedGroups = generatedCommandGroups.map((g) => {
30
33
  return { ...g, commands: [...commands, getUserTaskFormCmd] };
31
34
  }
32
35
  if (g === jobGroup) {
33
- return { ...g, commands: [...g.commands, workerCmd] };
36
+ return g;
34
37
  }
35
38
  return g;
36
39
  });
37
40
  const sortedOtherGroups = [
38
41
  connectorGroup,
39
- pluginGroup,
40
42
  ...customisedGroups,
41
43
  ...adminCommandGroups,
42
44
  completionGroup,
43
45
  ].sort((a, b) => a.name.localeCompare(b.name));
44
- export const commandGroups = [
46
+ const workerGroup = {
47
+ name: "worker",
48
+ description: workerCmd.description,
49
+ commands: [workerCmd],
50
+ };
51
+ /** Pinned groups shown above the separator in the main TUI menu. */
52
+ export const pinnedGroups = [
45
53
  askGroup,
54
+ lintGroup,
55
+ storyGroup,
46
56
  settingsGroup,
47
- profileGroup,
48
- ...sortedOtherGroups,
57
+ testGroup,
58
+ workerGroup,
49
59
  ];
60
+ /** API command groups — shown below the plugin section in the main TUI menu. */
61
+ export const apiGroups = sortedOtherGroups;
62
+ /** All built-in groups — used for CLI routing. */
63
+ export const commandGroups = [...pinnedGroups, ...apiGroups];
64
+ // Exported for CLI routing in run.ts (not shown in main TUI menu)
65
+ export { pluginGroup, profileGroup };
50
66
  // Compute follow-up relations between commands based on shared field/arg names
51
67
  computeRelations(commandGroups);
52
68
  // Manually inject relations on GET commands (they return a single object, not a
@@ -0,0 +1,219 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { Bpmn, compactify, optimize } from "@bpmnkit/core";
3
+ const SEVERITY_SYMBOL = {
4
+ error: "✖",
5
+ warning: "⚠",
6
+ info: "ℹ",
7
+ };
8
+ const DEFAULT_SERVER = "http://localhost:3033";
9
+ const lintCmd = {
10
+ name: "lint",
11
+ description: "Lint a BPMN file — run all static analysis and pattern checks",
12
+ args: [{ name: "file", description: "Path to the .bpmn file", required: true }],
13
+ flags: [
14
+ {
15
+ name: "categories",
16
+ description: "Comma-separated categories to run (default: all)",
17
+ type: "string",
18
+ },
19
+ {
20
+ name: "format",
21
+ description: "Output format: text (default) or json",
22
+ type: "string",
23
+ },
24
+ {
25
+ name: "fix",
26
+ description: "Auto-apply all fixable findings and write the result back to the file",
27
+ type: "boolean",
28
+ },
29
+ ],
30
+ async run(ctx) {
31
+ const filePath = ctx.positional[0];
32
+ if (!filePath)
33
+ throw new Error("Missing required argument: <file>");
34
+ const xml = await readFile(filePath, "utf-8");
35
+ const defs = Bpmn.parse(xml);
36
+ const categoriesFlag = ctx.flags.categories;
37
+ const categories = typeof categoriesFlag === "string" && categoriesFlag.length > 0
38
+ ? categoriesFlag.split(",").map((s) => s.trim())
39
+ : undefined;
40
+ const report = optimize(defs, categories !== undefined ? { categories } : undefined);
41
+ const { findings } = report;
42
+ // --fix: apply all auto-fixable findings and write back
43
+ if (ctx.flags.fix) {
44
+ const fixable = findings.filter((f) => f.applyFix);
45
+ for (const f of fixable)
46
+ f.applyFix?.(defs);
47
+ if (fixable.length === 0) {
48
+ ctx.output.ok("No auto-fixable issues found.");
49
+ }
50
+ else {
51
+ await writeFile(filePath, Bpmn.export(defs), "utf-8");
52
+ ctx.output.ok(`Fixed ${fixable.length} issue${fixable.length === 1 ? "" : "s"} and wrote ${filePath}`);
53
+ }
54
+ return;
55
+ }
56
+ const formatFlag = ctx.flags.format;
57
+ if (formatFlag === "json") {
58
+ ctx.output.print(findings);
59
+ return;
60
+ }
61
+ if (findings.length === 0) {
62
+ ctx.output.ok("No issues found.");
63
+ return;
64
+ }
65
+ for (const f of findings) {
66
+ const symbol = SEVERITY_SYMBOL[f.severity] ?? "·";
67
+ const elIds = f.elementIds.length > 0 ? ` [${f.elementIds.join(", ")}]` : "";
68
+ ctx.output.info(`${symbol} [${f.category}]${elIds} ${f.message}`);
69
+ }
70
+ const { total, bySeverity } = report.summary;
71
+ const errorCount = bySeverity.error ?? 0;
72
+ const warnCount = bySeverity.warning ?? 0;
73
+ const infoCount = bySeverity.info ?? 0;
74
+ ctx.output.info(`\n${total} finding${total !== 1 ? "s" : ""}: ${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warnCount} warning${warnCount !== 1 ? "s" : ""}, ${infoCount} info`);
75
+ if (errorCount > 0) {
76
+ throw new Error(`Lint failed with ${errorCount} error${errorCount !== 1 ? "s" : ""}`);
77
+ }
78
+ },
79
+ };
80
+ function describeOp(op) {
81
+ switch (op.op) {
82
+ case "rename":
83
+ return `Rename to "${op.name}"`;
84
+ case "update":
85
+ return "Update element properties";
86
+ case "delete":
87
+ return "Remove element";
88
+ case "insert":
89
+ return `Add ${op.element.type}${op.element.name ? ` "${op.element.name}"` : ""}`;
90
+ case "add_flow":
91
+ return `Add flow: ${op.from} → ${op.to}`;
92
+ case "delete_flow":
93
+ return "Remove flow";
94
+ case "redirect_flow":
95
+ return "Redirect flow";
96
+ }
97
+ }
98
+ const improveCmd = {
99
+ name: "improve",
100
+ description: "AI-assisted BPMN improvement — analyzes and suggests fixes using an AI model",
101
+ args: [{ name: "file", description: "Path to the .bpmn file", required: true }],
102
+ flags: [
103
+ {
104
+ name: "auto",
105
+ description: "Apply the AI-suggested improvements and write the result back to the file",
106
+ type: "boolean",
107
+ },
108
+ {
109
+ name: "server",
110
+ description: `AI proxy server URL (default: ${DEFAULT_SERVER})`,
111
+ type: "string",
112
+ },
113
+ ],
114
+ async run(ctx) {
115
+ const filePath = ctx.positional[0];
116
+ if (!filePath)
117
+ throw new Error("Missing required argument: <file>");
118
+ const serverUrl = typeof ctx.flags.server === "string" && ctx.flags.server.length > 0
119
+ ? ctx.flags.server
120
+ : DEFAULT_SERVER;
121
+ const xml = await readFile(filePath, "utf-8");
122
+ const defs = Bpmn.parse(xml);
123
+ const compactDiagram = compactify(defs);
124
+ let res;
125
+ try {
126
+ res = await fetch(`${serverUrl}/improve`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ context: compactDiagram, instruction: null, backend: null }),
130
+ });
131
+ }
132
+ catch (err) {
133
+ throw new Error(`Cannot reach AI server at ${serverUrl}. Start it with: pnpx @bpmnkit/ai-server\n${String(err)}`);
134
+ }
135
+ if (!res.ok || !res.body) {
136
+ throw new Error(`AI server returned ${res.status}`);
137
+ }
138
+ let capturedOps = [];
139
+ let capturedAutoFixCount = 0;
140
+ let capturedXml;
141
+ let hadOutput = false;
142
+ const reader = res.body.getReader();
143
+ const decoder = new TextDecoder();
144
+ let buf = "";
145
+ try {
146
+ while (true) {
147
+ const { done, value } = await reader.read();
148
+ if (done)
149
+ break;
150
+ buf += decoder.decode(value, { stream: true });
151
+ const parts = buf.split("\n\n");
152
+ buf = parts.pop() ?? "";
153
+ for (const part of parts) {
154
+ const line = part.startsWith("data: ") ? part.slice(6) : part;
155
+ const trimmed = line.trim();
156
+ if (!trimmed)
157
+ continue;
158
+ try {
159
+ const event = JSON.parse(trimmed);
160
+ if (event.type === "token" && event.text) {
161
+ process.stdout.write(event.text);
162
+ hadOutput = true;
163
+ }
164
+ if (event.type === "ops") {
165
+ capturedOps = event.ops ?? [];
166
+ capturedAutoFixCount = event.autoFixCount ?? 0;
167
+ }
168
+ if (event.type === "xml" && event.xml)
169
+ capturedXml = event.xml;
170
+ if (event.type === "error")
171
+ throw new Error(event.message ?? "AI error");
172
+ }
173
+ catch (e) {
174
+ if (e instanceof SyntaxError)
175
+ continue;
176
+ throw e;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ finally {
182
+ reader.releaseLock();
183
+ }
184
+ if (hadOutput)
185
+ process.stdout.write("\n\n");
186
+ // Print diff summary
187
+ if (capturedAutoFixCount > 0) {
188
+ ctx.output.info(`✓ ${capturedAutoFixCount} issue${capturedAutoFixCount === 1 ? "" : "s"} auto-fixed`);
189
+ }
190
+ if (capturedOps.length > 0) {
191
+ ctx.output.info("AI-suggested changes:");
192
+ for (const op of capturedOps) {
193
+ const prefix = op.op === "insert" || op.op === "add_flow"
194
+ ? "+"
195
+ : op.op === "delete" || op.op === "delete_flow"
196
+ ? "−"
197
+ : "~";
198
+ ctx.output.info(` ${prefix} ${describeOp(op)}`);
199
+ }
200
+ }
201
+ if (capturedXml === undefined) {
202
+ ctx.output.info("No changes suggested.");
203
+ return;
204
+ }
205
+ if (ctx.flags.auto) {
206
+ await writeFile(filePath, capturedXml, "utf-8");
207
+ ctx.output.ok(`Improvements applied and written to ${filePath}`);
208
+ }
209
+ else {
210
+ ctx.output.info("\nRun with --auto to apply these changes.");
211
+ }
212
+ },
213
+ };
214
+ export const lintGroup = {
215
+ name: "lint",
216
+ description: "Lint BPMN files using the static analyzer",
217
+ commands: [lintCmd, improveCmd],
218
+ };
219
+ //# sourceMappingURL=lint.js.map
@@ -17,7 +17,7 @@ function runNpm(args, cwd) {
17
17
  child.on("error", reject);
18
18
  });
19
19
  }
20
- async function searchNpmRegistry(query) {
20
+ export async function searchNpmRegistry(query) {
21
21
  const text = `keywords:casen-plugin${query ? ` ${query}` : ""}`;
22
22
  const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
23
23
  const res = await fetch(url);
@@ -1,12 +1,27 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { deleteProfile, getActiveName, getActiveProfile, getConfigFilePath, getProfile, listProfiles, saveProfile, useProfile, } from "@bpmnkit/profiles";
2
+ import { deleteProfile, getActiveName, getActiveProfile, getConfigFilePath, getProfile, listProfiles, saveProfile, setProfileMeta, useProfile, } from "@bpmnkit/profiles";
3
3
  const API_TYPE_FLAG = {
4
4
  name: "api-type",
5
5
  description: "API type: c8 (default) or admin",
6
6
  type: "string",
7
7
  default: "c8",
8
8
  placeholder: "TYPE",
9
+ enum: ["c8", "admin"],
9
10
  };
11
+ const META_FLAGS = [
12
+ {
13
+ name: "description",
14
+ description: "Short description of the profile",
15
+ type: "string",
16
+ placeholder: "TEXT",
17
+ },
18
+ {
19
+ name: "tags",
20
+ description: "Comma-separated tags (predefined: dev, stage, prod)",
21
+ type: "string",
22
+ placeholder: "TAGS",
23
+ },
24
+ ];
10
25
  const AUTH_FLAGS = [
11
26
  {
12
27
  name: "base-url",
@@ -21,6 +36,7 @@ const AUTH_FLAGS = [
21
36
  type: "string",
22
37
  required: true,
23
38
  placeholder: "TYPE",
39
+ enum: ["bearer", "oauth2", "basic", "none"],
24
40
  },
25
41
  {
26
42
  name: "token",
@@ -165,7 +181,7 @@ export const profileGroup = {
165
181
  name: "create",
166
182
  description: "Create or update a profile",
167
183
  args: [{ name: "name", description: "Profile name", required: true }],
168
- flags: [API_TYPE_FLAG, ...AUTH_FLAGS],
184
+ flags: [API_TYPE_FLAG, ...AUTH_FLAGS, ...META_FLAGS],
169
185
  examples: [
170
186
  {
171
187
  description: "Bearer token profile",
@@ -191,7 +207,15 @@ export const profileGroup = {
191
207
  const rawApiType = ctx.flags["api-type"] ?? "c8";
192
208
  const apiType = rawApiType === "admin" ? "admin" : "c8";
193
209
  const config = { baseUrl, auth };
194
- saveProfile(name, config, apiType);
210
+ const description = ctx.flags.description;
211
+ const tagsRaw = ctx.flags.tags;
212
+ const tags = tagsRaw
213
+ ? tagsRaw
214
+ .split(",")
215
+ .map((t) => t.trim())
216
+ .filter(Boolean)
217
+ : undefined;
218
+ saveProfile(name, config, apiType, { description, tags });
195
219
  ctx.output.ok(`Profile "${name}" saved [${apiType}] (${getConfigFilePath()})`);
196
220
  },
197
221
  },
@@ -214,6 +238,7 @@ export const profileGroup = {
214
238
  apiType: p.apiType,
215
239
  baseUrl: p.config.baseUrl ?? "(from env/file)",
216
240
  authType: p.config.auth?.type ?? "—",
241
+ tags: p.tags && p.tags.length > 0 ? p.tags.join(", ") : "—",
217
242
  })),
218
243
  }, [
219
244
  { key: "active", header: " " },
@@ -221,6 +246,7 @@ export const profileGroup = {
221
246
  { key: "apiType", header: "API" },
222
247
  { key: "baseUrl", header: "BASE URL", maxWidth: 50 },
223
248
  { key: "authType", header: "AUTH TYPE" },
249
+ { key: "tags", header: "TAGS" },
224
250
  ]);
225
251
  },
226
252
  },
@@ -260,6 +286,10 @@ export const profileGroup = {
260
286
  const active = getActiveName();
261
287
  const isActive = profile.name === active;
262
288
  ctx.output.info(`Profile: ${profile.name}${isActive ? " (active)" : ""}`);
289
+ if (profile.description)
290
+ ctx.output.info(`Description: ${profile.description}`);
291
+ if (profile.tags && profile.tags.length > 0)
292
+ ctx.output.info(`Tags: ${profile.tags.join(", ")}`);
263
293
  ctx.output.printItem(profile.config);
264
294
  },
265
295
  },
@@ -304,6 +334,35 @@ export const profileGroup = {
304
334
  ctx.output.info(`baseUrl: ${config.baseUrl ?? ""}`);
305
335
  },
306
336
  },
337
+ {
338
+ name: "meta",
339
+ description: "Update description or tags on an existing profile",
340
+ args: [{ name: "name", description: "Profile name", required: true }],
341
+ flags: META_FLAGS,
342
+ examples: [
343
+ {
344
+ description: "Set description and tags",
345
+ command: 'casen profile meta prod --description "Production cluster" --tags prod',
346
+ },
347
+ ],
348
+ async run(ctx) {
349
+ const name = ctx.positional[0];
350
+ if (!name)
351
+ throw new Error("Missing required argument: <name>");
352
+ const description = ctx.flags.description;
353
+ const tagsRaw = ctx.flags.tags;
354
+ const tags = tagsRaw
355
+ ? tagsRaw
356
+ .split(",")
357
+ .map((t) => t.trim())
358
+ .filter(Boolean)
359
+ : undefined;
360
+ if (!setProfileMeta(name, { description, tags })) {
361
+ throw new Error(`Profile "${name}" not found.`);
362
+ }
363
+ ctx.output.ok(`Profile "${name}" updated`);
364
+ },
365
+ },
307
366
  {
308
367
  name: "delete",
309
368
  aliases: ["rm"],
@@ -0,0 +1,42 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { Bpmn, renderStoryHtml } from "@bpmnkit/core";
3
+ const storyCmd = {
4
+ name: "story",
5
+ description: "Render a BPMN process as a standalone story-mode HTML file",
6
+ args: [{ name: "file", description: "Path to the .bpmn file", required: true }],
7
+ flags: [
8
+ {
9
+ name: "output",
10
+ short: "o",
11
+ description: "Output path (default: <file>.story.html)",
12
+ type: "string",
13
+ },
14
+ {
15
+ name: "theme",
16
+ description: "Color theme: light (default) or dark",
17
+ type: "string",
18
+ },
19
+ ],
20
+ async run(ctx) {
21
+ const filePath = ctx.positional[0];
22
+ if (!filePath)
23
+ throw new Error("Missing required argument: <file>");
24
+ const xml = await readFile(filePath, "utf-8");
25
+ const defs = Bpmn.parse(xml);
26
+ const themeFlag = ctx.flags.theme;
27
+ const theme = themeFlag === "dark" ? "dark" : "light";
28
+ const outputFlag = ctx.flags.output;
29
+ const outputPath = typeof outputFlag === "string" && outputFlag.length > 0
30
+ ? outputFlag
31
+ : `${filePath}.story.html`;
32
+ const html = renderStoryHtml(defs, { standalone: true, theme });
33
+ await writeFile(outputPath, html, "utf-8");
34
+ ctx.output.ok(`Story HTML written to ${outputPath}`);
35
+ },
36
+ };
37
+ export const storyGroup = {
38
+ name: "story",
39
+ description: "Render BPMN processes as narrative HTML",
40
+ commands: [storyCmd],
41
+ };
42
+ //# sourceMappingURL=story.js.map
@@ -0,0 +1,77 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { Bpmn } from "@bpmnkit/core";
3
+ import { Engine, runScenario } from "@bpmnkit/engine";
4
+ const testCmd = {
5
+ name: "test",
6
+ description: "Run scenario tests for a BPMN process file",
7
+ args: [
8
+ {
9
+ name: "file",
10
+ description: "Path to the .bpmn file",
11
+ required: true,
12
+ },
13
+ ],
14
+ flags: [
15
+ {
16
+ name: "scenarios",
17
+ short: "s",
18
+ description: "Path to the .bpmn.tests.json scenarios file (default: <file>.tests.json)",
19
+ type: "string",
20
+ },
21
+ ],
22
+ async run(ctx) {
23
+ const bpmnPath = ctx.positional[0];
24
+ if (bpmnPath === undefined)
25
+ throw new Error("Missing required argument: <file>");
26
+ const scenariosPath = typeof ctx.flags.scenarios === "string" ? ctx.flags.scenarios : `${bpmnPath}.tests.json`;
27
+ const bpmnXml = await readFile(bpmnPath, "utf8").catch(() => {
28
+ throw new Error(`Cannot read BPMN file: ${bpmnPath}`);
29
+ });
30
+ const scenariosRaw = await readFile(scenariosPath, "utf8").catch(() => {
31
+ throw new Error(`Cannot read scenarios file: ${scenariosPath}`);
32
+ });
33
+ let scenarios;
34
+ try {
35
+ scenarios = JSON.parse(scenariosRaw);
36
+ }
37
+ catch {
38
+ throw new Error(`Invalid JSON in scenarios file: ${scenariosPath}`);
39
+ }
40
+ if (!Array.isArray(scenarios) || scenarios.length === 0) {
41
+ ctx.output.info("No scenarios found.");
42
+ return;
43
+ }
44
+ const defs = Bpmn.parse(bpmnXml);
45
+ const engine = new Engine();
46
+ let passed = 0;
47
+ let failed = 0;
48
+ for (const scenario of scenarios) {
49
+ const result = await runScenario(engine, defs, scenario);
50
+ if (result.passed) {
51
+ passed++;
52
+ ctx.output.ok(`PASS ${scenario.name} (${result.durationMs}ms)`);
53
+ }
54
+ else {
55
+ failed++;
56
+ ctx.output.info(`FAIL ${scenario.name} (${result.durationMs}ms)`);
57
+ for (const f of result.failures) {
58
+ ctx.output.info(` ${f.field}: expected ${JSON.stringify(f.expected)}, got ${JSON.stringify(f.actual)}`);
59
+ }
60
+ for (const e of result.errors) {
61
+ ctx.output.info(` error${e.elementId !== undefined ? ` (${e.elementId})` : ""}: ${e.message}`);
62
+ }
63
+ }
64
+ }
65
+ const total = passed + failed;
66
+ ctx.output.info(`\n${passed}/${total} passed`);
67
+ if (failed > 0) {
68
+ throw new Error(`${failed} scenario(s) failed`);
69
+ }
70
+ },
71
+ };
72
+ export const testGroup = {
73
+ name: "test",
74
+ description: "Run scenario-based tests for BPMN processes",
75
+ commands: [testCmd],
76
+ };
77
+ //# sourceMappingURL=test.js.map
@@ -11,11 +11,13 @@
11
11
  export const workerCmd = {
12
12
  name: "worker",
13
13
  description: "Run a simple job worker that auto-completes jobs of a given type",
14
+ _worker: { jobType: "io.camunda.connector.HttpJson:1" },
14
15
  args: [
15
16
  {
16
17
  name: "type",
17
18
  description: "Job type to subscribe to (matches the task definition type in BPMN)",
18
19
  required: true,
20
+ default: "io.camunda.connector.HttpJson:1",
19
21
  },
20
22
  ],
21
23
  flags: [
@@ -46,11 +48,11 @@ export const workerCmd = {
46
48
  examples: [
47
49
  {
48
50
  description: "Subscribe to jobs of type 'payment-service'",
49
- command: "casen job worker payment-service",
51
+ command: "casen worker payment-service",
50
52
  },
51
53
  {
52
54
  description: "Return custom variables on completion",
53
- command: 'casen job worker payment-service --variables \'{"status":"ok","amount":100}\'',
55
+ command: 'casen worker payment-service --variables \'{"status":"ok","amount":100}\'',
54
56
  },
55
57
  ],
56
58
  async run(ctx) {
@@ -92,7 +94,6 @@ export const workerCmd = {
92
94
  worker: "casen-worker",
93
95
  timeout,
94
96
  maxJobsToActivate: maxJobs,
95
- requestTimeout: 20000, // 20 s long poll
96
97
  }));
97
98
  }
98
99
  catch (err) {
@@ -107,24 +108,27 @@ export const workerCmd = {
107
108
  for (const job of jobs) {
108
109
  if (!running)
109
110
  break;
110
- ctx.output.info(`Activated job ${job.jobKey} process=${job.processDefinitionId} element=${job.elementId} instance=${job.processInstanceKey}`);
111
+ // Some Zeebe-compatible engines return `key` instead of `jobKey`
112
+ const jobKey = job.jobKey ?? job.key;
113
+ if (!jobKey) {
114
+ ctx.output.info("Activated job has no key — skipping");
115
+ continue;
116
+ }
117
+ ctx.output.info(`Activated job ${jobKey} process=${job.processDefinitionId} element=${job.elementId} instance=${job.processInstanceKey}`);
111
118
  if (Object.keys(job.variables ?? {}).length > 0) {
112
119
  ctx.output.info(` Input variables: ${JSON.stringify(job.variables)}`);
113
120
  }
114
121
  try {
115
- await client.job.completeJob(job.jobKey, { variables });
122
+ await client.job.completeJob(jobKey, { variables });
116
123
  completed++;
117
- ctx.output.ok(`Completed job ${job.jobKey} (total: ${completed})`);
124
+ ctx.output.ok(`Completed job ${jobKey} (total: ${completed})`);
118
125
  }
119
126
  catch (err) {
120
- ctx.output.info(`Failed to complete job ${job.jobKey}: ${err instanceof Error ? err.message : String(err)}`);
127
+ ctx.output.info(`Failed to complete job ${jobKey}: ${err instanceof Error ? err.message : String(err)}`);
121
128
  }
122
129
  }
123
- // When no jobs were returned, the long-poll already waited; loop immediately.
124
- // When jobs were found, yield the event loop briefly before the next poll.
125
- if (jobs.length > 0) {
126
- await delay(100);
127
- }
130
+ // Sleep between polls: brief yield after processing jobs, longer pause when idle
131
+ await delay(jobs.length > 0 ? 100 : 2000);
128
132
  }
129
133
  ctx.output.info(`\nWorker stopped. Completed ${completed} job(s).`);
130
134
  },
@@ -53,6 +53,7 @@ export const authorizationGroup = {
53
53
  makeCreateCmd({
54
54
  description: "Create authorization",
55
55
  create: (client, body) => client.authorization.createAuthorization(body),
56
+ bodyFields: [{ name: "ownerId", type: "string", description: "The ID of the owner of the permissions.", required: true }, { name: "ownerType", type: "string", description: "The type of the owner of permissions.", required: true, enum: ["USER", "CLIENT", "ROLE", "GROUP", "MAPPING_RULE", "UNSPECIFIED"] }, { name: "resourceId", type: "string", description: "The ID of the resource to add permissions to.", required: true }, { name: "resourceType", type: "string", description: "The type of resource to add permissions to.", required: true }, { name: "permissionTypes", type: "array", description: "The permission types to add.", required: true }, { name: "resourcePropertyName", type: "string", description: "The name of the resource property on which this authorization is based.", required: true }],
56
57
  }),
57
58
  makeListCmd({
58
59
  description: "Search authorizations",
@@ -77,6 +78,7 @@ export const authorizationGroup = {
77
78
  description: "Update authorization",
78
79
  argName: "authorizationKey",
79
80
  update: (client, key, body) => client.authorization.updateAuthorization(key, body),
81
+ bodyFields: [{ name: "ownerId", type: "string", description: "The ID of the owner of the permissions.", required: true }, { name: "ownerType", type: "string", description: "The type of the owner of permissions.", required: true, enum: ["USER", "CLIENT", "ROLE", "GROUP", "MAPPING_RULE", "UNSPECIFIED"] }, { name: "resourceId", type: "string", description: "The ID of the resource to add permissions to.", required: true }, { name: "resourceType", type: "string", description: "The type of resource to add permissions to.", required: true }, { name: "permissionTypes", type: "array", description: "The permission types to add.", required: true }, { name: "resourcePropertyName", type: "string", description: "The name of the resource property on which this authorization is based.", required: true }],
80
82
  }),
81
83
  makeDeleteCmd({
82
84
  description: "Delete authorization",
@@ -354,6 +356,7 @@ export const decisionDefinitionGroup = {
354
356
  name: "evaluate",
355
357
  description: "Evaluate decision",
356
358
  create: (client, body) => client.decisionDefinition.evaluateDecision(body),
359
+ bodyFields: [{ name: "decisionDefinitionId", type: "string", description: " The ID of the decision to be evaluated.\n When using the decision ID, the latest\n deployed version of the decision is used.", required: true }, { name: "variables", type: "object", description: "The message variables as JSON document." }, { name: "tenantId", type: "string", description: "The tenant ID of the decision." }, { name: "decisionDefinitionKey", type: "string", description: "System-generated key for a decision definition.", required: true }],
357
360
  }),
358
361
  makeListCmd({
359
362
  description: "Search decision definitions",
@@ -1158,6 +1161,7 @@ export const processInstanceGroup = {
1158
1161
  makeCreateCmd({
1159
1162
  description: "Create process instance",
1160
1163
  create: (client, body) => client.processInstance.createProcessInstance(body),
1164
+ bodyFields: [{ name: "processDefinitionKey", type: "string", description: " The unique key identifying the process definition, for example, returned for a process in the\n deploy resources endpoint.", required: true }, { name: "processDefinitionVersion", type: "string", description: " As the version is already identified by the `processDefinitionKey`, the value of this field is ignored.\n It's here for backwards-compatibility only as previous releases accepted it in request bodies." }, { name: "variables", type: "object", description: " Set of variables as JSON object to instantiate in the root variable scope of the process\n instance. Can include nested complex objects." }, { name: "startInstructions", type: "array", description: " List of start instructions. By default, the process instance will start at\n the start event. If provided, the process instance will apply start instructions\n after it has been created." }, { name: "runtimeInstructions", type: "array", description: " Runtime instructions (alpha). List of instructions that affect the runtime behavior of\n the process instance. Refer to specific instruction types for more details.\n\n This parameter is an alpha feature and may be subject to change\n in future releases." }, { name: "tenantId", type: "string", description: " The tenant id of the process definition.\n If multi-tenancy is enabled, provide the tenant id of the process definition to start a\n process instance of. If multi-tenancy is disabled, don't provide this parameter." }, { name: "operationReference", type: "string", description: " A reference key chosen by the user that will be part of all records resulting from this operation.\n Must be > 0 if provided." }, { name: "awaitCompletion", type: "boolean", description: " Wait for the process instance to complete. If the process instance does not complete\n within the request timeout limit, a 504 response status will be returned. The process\n instance will continue to run in the background regardless of the timeout. Disabled by\n default." }, { name: "requestTimeout", type: "string", description: " Timeout (in ms) the request waits for the process to complete. By default or\n when set to 0, the generic request timeout configured in the cluster is applied." }, { name: "fetchVariables", type: "array", description: " List of variables by name to be included in the response when awaitCompletion is set to true.\n If empty, all visible variables in the root scope will be returned." }, { name: "tags", type: "array", description: "List of tags. Tags need to start with a letter; then alphanumerics, `_`, `-`, `:`, or `.`; length ≤ 100." }, { name: "businessId", type: "string", description: " An optional, user-defined string identifier that identifies the process instance\n within the scope of a process definition (scoped by tenant). If provided and uniqueness\n enforcement is enabled, the engine will reject creation if another root process instance\n with the same business id is already active for the same process definition.\n Note that any active child process instances with the same business id are not taken into account." }, { name: "processDefinitionId", type: "string", description: " The BPMN process id of the process definition to start an instance of.", required: true }],
1161
1165
  }),
1162
1166
  makeCreateCmd({
1163
1167
  name: "cancel-batch-operation",