camunda-cli 0.2.0 → 0.3.1
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 +31 -5
- package/bin/camunda.js +12 -9
- package/package.json +1 -1
- package/src/commands/definitions.js +51 -26
- package/src/commands/diagnose.js +7 -4
- package/src/commands/instances.js +51 -23
- package/src/commands/tasks.js +13 -4
- package/src/followup.js +60 -0
- package/src/lint.js +25 -1
package/README.md
CHANGED
|
@@ -63,7 +63,15 @@ Sequence flows
|
|
|
63
63
|
Gateway_1jk90v6 Activity_retry no ${paid == false}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
**Find the bugs before an instance does.** `lint` checks
|
|
66
|
+
**Find the bugs before an instance does.** `lint` checks a model statically. It takes a
|
|
67
|
+
deployed key or a local file, so the usual loop is to check while editing and only then
|
|
68
|
+
deploy:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
camunda lint ./order-process.bpmn # no engine and no login needed
|
|
72
|
+
camunda lint order-process # or the deployed version
|
|
73
|
+
```
|
|
74
|
+
|
|
67
75
|
Every rule exists because that failure was reproduced against a real engine first:
|
|
68
76
|
|
|
69
77
|
| Rule | What it catches |
|
|
@@ -132,10 +140,28 @@ engine's key-based endpoints answer *"no matching process definition ... and no
|
|
|
132
140
|
which reads like the process is missing when it is not. Every command taking a key accepts
|
|
133
141
|
`--tenant`, and an ambiguous key lists the candidates rather than guessing.
|
|
134
142
|
|
|
135
|
-
**`start`
|
|
136
|
-
accepted it; anything marked async runs after the response.
|
|
137
|
-
moment
|
|
138
|
-
|
|
143
|
+
**`start` and `complete` report what happened next.** A start returning HTTP 200 only means
|
|
144
|
+
the engine accepted it; anything marked async runs after the response. Both commands wait a
|
|
145
|
+
moment, then say whether it failed, whether the instance finished, or which task is now
|
|
146
|
+
waiting, along with the exact command to complete it:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
$ camunda start order-process --var amount=700:Integer
|
|
150
|
+
Started order-process v9 as instance 3435051
|
|
151
|
+
|
|
152
|
+
Now waiting at:
|
|
153
|
+
3435058 check stock ops
|
|
154
|
+
camunda complete 3435058 --var quantity=<value>
|
|
155
|
+
|
|
156
|
+
$ camunda complete 3435058 --var quantity=3
|
|
157
|
+
Task 3435058 completed.
|
|
158
|
+
Instance 3435051 finished in 8.0s.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`--no-wait` skips the follow-up.
|
|
162
|
+
|
|
163
|
+
**Cleaning up after a test run.** `cancel --key <key>` terminates every running instance of
|
|
164
|
+
a process in one go, which matters because testing a model leaves a trail of them behind.
|
|
139
165
|
|
|
140
166
|
**Variable types matter.** `--var n=300` sends a string, and `"300" > 200` is a string
|
|
141
167
|
comparison. Use `--var n=300:Integer` where a gateway compares numerically, or `name:=<json>`
|
package/bin/camunda.js
CHANGED
|
@@ -24,7 +24,7 @@ program
|
|
|
24
24
|
'Start with "camunda inspect <key>" to read a deployed model, and\n' +
|
|
25
25
|
'"camunda diagnose <instanceId>" when an instance misbehaves.'
|
|
26
26
|
)
|
|
27
|
-
.version('0.
|
|
27
|
+
.version('0.3.1')
|
|
28
28
|
.option('--json', 'print the raw API payload instead of a formatted view')
|
|
29
29
|
.option('--no-color', 'never emit colour, even on a terminal')
|
|
30
30
|
.showHelpAfterError()
|
|
@@ -66,7 +66,7 @@ withVersion(
|
|
|
66
66
|
withTenant(
|
|
67
67
|
program
|
|
68
68
|
.command('inspect <keyOrId>')
|
|
69
|
-
.description('Structure of a
|
|
69
|
+
.description('Structure of a model: elements, flow conditions, forms, integrations. Accepts a deployed key or a local .bpmn file')
|
|
70
70
|
.option('--no-lint', 'skip the static-check summary at the end')
|
|
71
71
|
)
|
|
72
72
|
).action(inspectCommand);
|
|
@@ -75,7 +75,7 @@ withVersion(
|
|
|
75
75
|
withTenant(
|
|
76
76
|
program
|
|
77
77
|
.command('lint <keyOrId>')
|
|
78
|
-
.description('Static checks over a
|
|
78
|
+
.description('Static checks over a model, before an instance has to find the bugs. Accepts a deployed key or a local .bpmn file')
|
|
79
79
|
.option('-a, --all', 'include informational findings')
|
|
80
80
|
.addOption(new Option('-s, --severity <level>', 'only one severity').choices(['error', 'warning', 'info']))
|
|
81
81
|
)
|
|
@@ -120,12 +120,15 @@ withVersion(
|
|
|
120
120
|
)
|
|
121
121
|
).action(startCommand);
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
withTenant(
|
|
124
|
+
program
|
|
125
|
+
.command('cancel [id]')
|
|
126
|
+
.description('Force-terminate one instance, or every instance of a process with --key')
|
|
127
|
+
.option('-k, --key <definitionKey>', 'cancel every running instance of this process')
|
|
128
|
+
.option('-b, --business-key <key>', 'with --key, only instances carrying this business key')
|
|
129
|
+
.option('-r, --reason <text>', 'recorded against the instances')
|
|
130
|
+
.option('-y, --yes', 'skip the confirmation prompt')
|
|
131
|
+
).action(cancelCommand);
|
|
129
132
|
|
|
130
133
|
program
|
|
131
134
|
.command('vars <instanceId>')
|
package/package.json
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { Client, resolveDefinition } from '../client.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { readProcess } from '../bpmn.js';
|
|
5
5
|
import { lintProcess } from '../lint.js';
|
|
6
6
|
import * as out from '../output.js';
|
|
7
7
|
|
|
8
|
+
// inspect and lint accept a local .bpmn file as well as a deployed key, because the point
|
|
9
|
+
// of checking a model is to do it while editing, before it reaches an engine. A local file
|
|
10
|
+
// needs no credentials either, so the config lookup only happens for the remote path.
|
|
11
|
+
function isLocalFile(target) {
|
|
12
|
+
return /\.(bpmn|xml|dmn)$/i.test(target) && existsSync(target) && statSync(target).isFile();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function loadModel(target, options) {
|
|
16
|
+
if (isLocalFile(target)) {
|
|
17
|
+
const xml = readFileSync(target, 'utf8');
|
|
18
|
+
return { model: readProcess(xml), xml, source: { local: target } };
|
|
19
|
+
}
|
|
20
|
+
const client = new Client(requireConfig());
|
|
21
|
+
const def = await resolveDefinition(client, target, options);
|
|
22
|
+
const { bpmn20Xml } = await client.processDefinitionXml(def.id);
|
|
23
|
+
return { model: readProcess(bpmn20Xml), xml: bpmn20Xml, source: { definition: def }, client };
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
export async function definitionsCommand(options) {
|
|
9
27
|
const client = new Client(requireConfig());
|
|
10
28
|
const query = { maxResults: options.limit ?? 50, sortBy: 'key', sortOrder: 'asc' };
|
|
@@ -27,32 +45,41 @@ export async function definitionsCommand(options) {
|
|
|
27
45
|
// Structural view of a deployed model. None of this is available as a REST resource:
|
|
28
46
|
// it is read out of the deployed BPMN XML, which is the only place the engine keeps it.
|
|
29
47
|
export async function inspectCommand(keyOrId, options) {
|
|
30
|
-
const client =
|
|
31
|
-
const def =
|
|
32
|
-
const { bpmn20Xml } = await client.processDefinitionXml(def.id);
|
|
33
|
-
const model = readProcess(bpmn20Xml);
|
|
48
|
+
const { model, source, client } = await loadModel(keyOrId, options);
|
|
49
|
+
const def = source.definition;
|
|
34
50
|
|
|
35
51
|
let stats = [];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
if (client && def) {
|
|
53
|
+
try {
|
|
54
|
+
stats = await client.processDefinitionStatistics(def.id);
|
|
55
|
+
} catch {
|
|
56
|
+
/* statistics are optional context, not worth failing the command over */
|
|
57
|
+
}
|
|
40
58
|
}
|
|
41
59
|
const liveByActivity = new Map(stats.map((s) => [s.id, s]));
|
|
42
60
|
|
|
43
61
|
if (out.isJsonMode()) {
|
|
44
|
-
return out.json({ definition: def, process: stripRaw(model), statistics: stats });
|
|
62
|
+
return out.json({ definition: def ?? { file: source.local }, process: stripRaw(model), statistics: stats });
|
|
45
63
|
}
|
|
46
64
|
|
|
47
|
-
out.heading(
|
|
48
|
-
out.kv(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
out.heading(def ? def.name || def.key : model.name || model.id);
|
|
66
|
+
out.kv(
|
|
67
|
+
def
|
|
68
|
+
? [
|
|
69
|
+
['key', def.key],
|
|
70
|
+
['id', def.id],
|
|
71
|
+
['version', def.version],
|
|
72
|
+
['tenant', def.tenantId ?? '-'],
|
|
73
|
+
['suspended', def.suspended ? 'yes' : 'no'],
|
|
74
|
+
['executable', model.executable ? 'yes' : 'no'],
|
|
75
|
+
]
|
|
76
|
+
: [
|
|
77
|
+
['file', source.local],
|
|
78
|
+
['process id', model.id],
|
|
79
|
+
['executable', model.executable ? 'yes' : 'no'],
|
|
80
|
+
['status', 'not deployed, read from disk'],
|
|
81
|
+
]
|
|
82
|
+
);
|
|
56
83
|
|
|
57
84
|
if (model.lanes.length > 0) {
|
|
58
85
|
out.line('\nLanes');
|
|
@@ -114,24 +141,22 @@ export async function inspectCommand(keyOrId, options) {
|
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
export async function lintCommand(keyOrId, options) {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const { bpmn20Xml } = await client.processDefinitionXml(def.id);
|
|
120
|
-
const model = readProcess(bpmn20Xml);
|
|
144
|
+
const { model, source } = await loadModel(keyOrId, options);
|
|
145
|
+
const label = source.definition ? `${source.definition.key} v${source.definition.version}` : source.local;
|
|
121
146
|
const findings = lintProcess(model);
|
|
122
147
|
|
|
123
148
|
const wanted = options.severity
|
|
124
149
|
? findings.filter((f) => f.severity === options.severity)
|
|
125
150
|
: findings.filter((f) => options.all || f.severity !== 'info');
|
|
126
151
|
|
|
127
|
-
if (out.isJsonMode()) return out.json({
|
|
152
|
+
if (out.isJsonMode()) return out.json({ source: label, findings: wanted });
|
|
128
153
|
|
|
129
154
|
if (wanted.length === 0) {
|
|
130
|
-
out.line(`${
|
|
155
|
+
out.line(`${label}: nothing to report.`);
|
|
131
156
|
return;
|
|
132
157
|
}
|
|
133
158
|
|
|
134
|
-
out.heading(
|
|
159
|
+
out.heading(label);
|
|
135
160
|
for (const f of wanted) {
|
|
136
161
|
const tag = f.severity === 'error' ? 'ERROR ' : f.severity === 'warning' ? 'WARNING' : 'INFO ';
|
|
137
162
|
out.line(`\n${tag} ${f.rule} ${f.element}`);
|
package/src/commands/diagnose.js
CHANGED
|
@@ -225,15 +225,18 @@ export async function stacktraceCommand(jobId, options) {
|
|
|
225
225
|
if (omitted > 0) out.note(` ... ${omitted} engine-internal frame(s) hidden, use --full for everything`);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// The execution trace
|
|
229
|
-
//
|
|
230
|
-
// the
|
|
228
|
+
// The execution trace, in the order the engine actually took the steps.
|
|
229
|
+
//
|
|
230
|
+
// Sorting by startTime looks right and is not: a gateway and the events either side of it
|
|
231
|
+
// routinely share a millisecond, and the resulting order is arbitrary within that tie, so
|
|
232
|
+
// a start event can appear halfway down the list. `occurrence` is the engine's own
|
|
233
|
+
// execution sequence and is the only field that reflects real order.
|
|
231
234
|
export async function traceCommand(id, options) {
|
|
232
235
|
const client = new Client(requireConfig());
|
|
233
236
|
const activities = await client.historyActivityInstances({
|
|
234
237
|
processInstanceId: id,
|
|
235
238
|
maxResults: options.limit ?? 200,
|
|
236
|
-
sortBy: '
|
|
239
|
+
sortBy: 'occurrence',
|
|
237
240
|
sortOrder: 'asc',
|
|
238
241
|
});
|
|
239
242
|
|
|
@@ -2,6 +2,8 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
import { stdin, stdout } from 'node:process';
|
|
3
3
|
import { Client, parseVariableFlags, resolveDefinition } from '../client.js';
|
|
4
4
|
import { requireConfig } from '../config.js';
|
|
5
|
+
import { readProcess } from '../bpmn.js';
|
|
6
|
+
import { reportNextState, suggestCompletion } from '../followup.js';
|
|
5
7
|
import * as out from '../output.js';
|
|
6
8
|
|
|
7
9
|
export async function instancesCommand(options) {
|
|
@@ -117,46 +119,72 @@ export async function startCommand(keyOrId, options) {
|
|
|
117
119
|
variables,
|
|
118
120
|
});
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
// after the response, so a failure there shows up as a job or incident a moment later
|
|
122
|
-
// rather than in this reply.
|
|
123
|
-
let trouble = { incidents: [], jobs: [] };
|
|
124
|
-
if (!options.noWait) {
|
|
125
|
-
await new Promise((r) => setTimeout(r, options.wait ?? 1200));
|
|
126
|
-
const [incidents, jobs] = await Promise.all([
|
|
127
|
-
client.incidents({ processInstanceId: result.id }).catch(() => []),
|
|
128
|
-
client.jobs({ processInstanceId: result.id, withException: true }).catch(() => []),
|
|
129
|
-
]);
|
|
130
|
-
trouble = { incidents, jobs };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (out.isJsonMode()) return out.json({ ...result, ...trouble });
|
|
122
|
+
if (out.isJsonMode() && options.noWait) return out.json(result);
|
|
134
123
|
|
|
135
124
|
out.line(`Started ${def.key} v${def.version} as instance ${result.id}`);
|
|
136
125
|
if (result.businessKey) out.note(`business key ${result.businessKey}`);
|
|
126
|
+
if (options.noWait) return;
|
|
137
127
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
128
|
+
// A start returning 200 only means the engine accepted it. Anything marked async runs
|
|
129
|
+
// after the response, so both a failure and the first user task appear a moment later
|
|
130
|
+
// rather than in this reply.
|
|
131
|
+
const next = await reportNextState(client, result.id, { wait: options.wait ?? 1200 });
|
|
132
|
+
if (next.failed) process.exitCode = 1;
|
|
133
|
+
else if (next.tasks.length === 1) {
|
|
134
|
+
out.note(await suggestCompletion(client, next.tasks[0], readProcess));
|
|
143
135
|
}
|
|
144
136
|
}
|
|
145
137
|
|
|
138
|
+
// Takes either one id or a whole definition's worth of instances. Testing a model leaves
|
|
139
|
+
// a trail of instances behind, and clearing them one confirmation at a time is enough
|
|
140
|
+
// friction that they tend to get left running instead.
|
|
146
141
|
export async function cancelCommand(id, options) {
|
|
147
142
|
const client = new Client(requireConfig());
|
|
148
143
|
|
|
144
|
+
if (!id && !options.key) {
|
|
145
|
+
throw new Error('Give an instance id, or --key <definitionKey> to cancel every running instance of one process.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let targets;
|
|
149
|
+
if (id) {
|
|
150
|
+
targets = [id];
|
|
151
|
+
} else {
|
|
152
|
+
const query = { processDefinitionKey: options.key, maxResults: 1000 };
|
|
153
|
+
if (options.tenant) query.tenantIdIn = options.tenant;
|
|
154
|
+
if (options.businessKey) query.businessKey = options.businessKey;
|
|
155
|
+
const found = await client.processInstances(query);
|
|
156
|
+
targets = found.map((i) => i.id);
|
|
157
|
+
if (targets.length === 0) return out.note(`No running instances of ${options.key}.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
149
160
|
if (!options.yes) {
|
|
150
161
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
162
|
+
const what =
|
|
163
|
+
targets.length === 1
|
|
164
|
+
? `Force-terminating ${targets[0]}`
|
|
165
|
+
: `Force-terminating all ${targets.length} running instances of ${options.key}`;
|
|
166
|
+
const expect = targets.length === 1 ? targets[0] : String(targets.length);
|
|
151
167
|
const answer = await rl.question(
|
|
152
|
-
|
|
168
|
+
`${what} cannot be undone (they end as EXTERNALLY_TERMINATED, not COMPLETED).\nType "${expect}" to confirm: `
|
|
153
169
|
);
|
|
154
170
|
rl.close();
|
|
155
|
-
if (answer.trim() !==
|
|
171
|
+
if (answer.trim() !== expect) return out.note('Aborted, that did not match.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let done = 0;
|
|
175
|
+
const failures = [];
|
|
176
|
+
for (const target of targets) {
|
|
177
|
+
try {
|
|
178
|
+
await client.deleteProcessInstance(target, { reason: options.reason });
|
|
179
|
+
done++;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
failures.push(`${target}: ${err.message}`);
|
|
182
|
+
}
|
|
156
183
|
}
|
|
157
184
|
|
|
158
|
-
|
|
159
|
-
out.
|
|
185
|
+
out.line(`Terminated ${done} instance(s).`);
|
|
186
|
+
for (const f of failures) out.problem(` ${f}`);
|
|
187
|
+
if (failures.length > 0) process.exitCode = 1;
|
|
160
188
|
}
|
|
161
189
|
|
|
162
190
|
export async function varsCommand(id, options) {
|
package/src/commands/tasks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client, parseVariableFlags } from '../client.js';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
3
|
import { readProcess } from '../bpmn.js';
|
|
4
|
+
import { reportNextState, suggestCompletion } from '../followup.js';
|
|
4
5
|
import { unwrapError, explain } from '../errors.js';
|
|
5
6
|
import * as out from '../output.js';
|
|
6
7
|
|
|
@@ -84,6 +85,13 @@ export async function completeCommand(id, options) {
|
|
|
84
85
|
const client = new Client(requireConfig());
|
|
85
86
|
const variables = parseVariableFlags(options.var);
|
|
86
87
|
|
|
88
|
+
// The task is gone once it completes, so its instance has to be noted beforehand for
|
|
89
|
+
// the follow-up to have something to report on.
|
|
90
|
+
const instanceId = await client
|
|
91
|
+
.task(id)
|
|
92
|
+
.then((t) => t.processInstanceId)
|
|
93
|
+
.catch(() => null);
|
|
94
|
+
|
|
87
95
|
try {
|
|
88
96
|
await client.completeTask(id, variables);
|
|
89
97
|
} catch (err) {
|
|
@@ -108,11 +116,12 @@ export async function completeCommand(id, options) {
|
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
out.line(`Task ${id} completed.`);
|
|
119
|
+
if (options.noWait || !instanceId) return;
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
const next = await reportNextState(client, instanceId, { wait: options.wait ?? 1000 });
|
|
122
|
+
if (next.failed) process.exitCode = 1;
|
|
123
|
+
else if (next.tasks.length === 1) {
|
|
124
|
+
out.note(await suggestCompletion(client, next.tasks[0], readProcess));
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
|
package/src/followup.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What happened after an action that moves a process forward.
|
|
2
|
+
//
|
|
3
|
+
// Both starting an instance and completing a task leave the caller needing the same three
|
|
4
|
+
// answers: did it break, did it finish, and what is waiting now. Without them every step of
|
|
5
|
+
// driving a process costs an extra round trip to work out where it went, which is most of
|
|
6
|
+
// the effort in testing a model with more than two steps.
|
|
7
|
+
|
|
8
|
+
import { unwrapError } from './errors.js';
|
|
9
|
+
import * as out from './output.js';
|
|
10
|
+
|
|
11
|
+
export async function reportNextState(client, instanceId, { wait = 1000 } = {}) {
|
|
12
|
+
if (wait) await new Promise((r) => setTimeout(r, wait));
|
|
13
|
+
|
|
14
|
+
const [historic, incidents, jobs, tasks] = await Promise.all([
|
|
15
|
+
client.historyProcessInstance(instanceId).catch(() => null),
|
|
16
|
+
client.incidents({ processInstanceId: instanceId }).catch(() => []),
|
|
17
|
+
client.jobs({ processInstanceId: instanceId, withException: true }).catch(() => []),
|
|
18
|
+
client.tasks({ processInstanceId: instanceId }).catch(() => []),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const failure = incidents[0]?.incidentMessage || jobs[0]?.exceptionMessage || null;
|
|
22
|
+
const state = historic?.state ?? null;
|
|
23
|
+
|
|
24
|
+
if (failure) {
|
|
25
|
+
out.problem(`\nIt failed after this step: ${unwrapError(failure).message}`);
|
|
26
|
+
out.note(`camunda diagnose ${instanceId}`);
|
|
27
|
+
return { state, failed: true, tasks };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (state && state !== 'ACTIVE') {
|
|
31
|
+
const label = state === 'COMPLETED' ? 'finished' : `ended as ${state}`;
|
|
32
|
+
out.line(`Instance ${instanceId} ${label}${historic.durationInMillis ? ` in ${out.formatDuration(historic.durationInMillis)}` : ''}.`);
|
|
33
|
+
return { state, failed: false, tasks: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (tasks.length > 0) {
|
|
37
|
+
out.line(tasks.length === 1 ? '\nNow waiting at:' : `\nNow waiting at ${tasks.length} tasks:`);
|
|
38
|
+
out.table(null, tasks.map((t) => [` ${t.id}`, out.truncate(t.name, 34), t.assignee ?? 'unassigned']));
|
|
39
|
+
return { state, failed: false, tasks };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
out.note(`\nStill running, not sitting at a user task. camunda instance ${instanceId}`);
|
|
43
|
+
return { state, failed: false, tasks: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The form fields a task needs, read from the deployed model. AlurKerja keeps its form in
|
|
47
|
+
// an extension attribute the engine's own form endpoints do not read, so this goes through
|
|
48
|
+
// the XML instead.
|
|
49
|
+
export async function suggestCompletion(client, task, readProcess) {
|
|
50
|
+
try {
|
|
51
|
+
const { bpmn20Xml } = await client.processDefinitionXml(task.processDefinitionId);
|
|
52
|
+
const model = readProcess(bpmn20Xml);
|
|
53
|
+
const node = model.nodes.find((n) => n.id === task.taskDefinitionKey);
|
|
54
|
+
const writable = (node?.formFields ?? []).filter((f) => !f.disabled);
|
|
55
|
+
if (writable.length === 0) return `camunda complete ${task.id}`;
|
|
56
|
+
return `camunda complete ${task.id} ${writable.map((f) => `--var ${f.name}=<value>`).join(' ')}`;
|
|
57
|
+
} catch {
|
|
58
|
+
return `camunda complete ${task.id}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/lint.js
CHANGED
|
@@ -108,7 +108,8 @@ export function lintProcess(model, { engineVariables = [] } = {}) {
|
|
|
108
108
|
n.id,
|
|
109
109
|
`Every outgoing flow of "${n.name || n.id}" has a condition and there is no default flow. ` +
|
|
110
110
|
`If they all evaluate false at runtime the engine raises ENGINE-02004 and the instance stops. ` +
|
|
111
|
-
`Mark one flow as the default
|
|
111
|
+
`Mark one flow as the default and remove its condition: a default flow carrying a condition is ` +
|
|
112
|
+
`rejected at deploy time with ENGINE-09005.`
|
|
112
113
|
);
|
|
113
114
|
|
|
114
115
|
// Numeric gap: a > N and a < N leave a == N with nowhere to go.
|
|
@@ -134,6 +135,29 @@ export function lintProcess(model, { engineVariables = [] } = {}) {
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
// The engine refuses to parse this rather than failing at runtime, so a model that is
|
|
139
|
+
// otherwise fine is rejected outright at deploy time.
|
|
140
|
+
if (n.defaultFlow) {
|
|
141
|
+
const target = outs.find((f) => f.id === n.defaultFlow);
|
|
142
|
+
if (!target) {
|
|
143
|
+
add(
|
|
144
|
+
'error',
|
|
145
|
+
'default-flow-missing',
|
|
146
|
+
n.id,
|
|
147
|
+
`"${n.name || n.id}" names "${n.defaultFlow}" as its default flow, but that is not one of its outgoing flows.`
|
|
148
|
+
);
|
|
149
|
+
} else if (target.condition) {
|
|
150
|
+
add(
|
|
151
|
+
'error',
|
|
152
|
+
'default-flow-with-condition',
|
|
153
|
+
n.id,
|
|
154
|
+
`The default flow of "${n.name || n.id}" ("${target.name || target.id}") also carries the condition ` +
|
|
155
|
+
`${target.condition}. A default flow is the branch taken when nothing else matches, so it cannot have ` +
|
|
156
|
+
`one: the engine rejects this at deploy time with ENGINE-09005. Remove the condition from that flow.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
137
161
|
if (n.type === 'exclusiveGateway' && unconditional.length > 1) {
|
|
138
162
|
add(
|
|
139
163
|
'warning',
|