camunda-cli 0.2.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/README.md +167 -0
- package/bin/camunda.js +292 -0
- package/package.json +36 -0
- package/src/bpmn.js +387 -0
- package/src/client.js +400 -0
- package/src/commands/definitions.js +194 -0
- package/src/commands/deploy.js +82 -0
- package/src/commands/diagnose.js +257 -0
- package/src/commands/instances.js +205 -0
- package/src/commands/ops.js +127 -0
- package/src/commands/session.js +112 -0
- package/src/commands/tasks.js +129 -0
- package/src/config.js +33 -0
- package/src/errors.js +138 -0
- package/src/lint.js +244 -0
- package/src/output.js +135 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Client } from '../client.js';
|
|
2
|
+
import { requireConfig } from '../config.js';
|
|
3
|
+
import { unwrapError, explain, condenseStacktrace } from '../errors.js';
|
|
4
|
+
import * as out from '../output.js';
|
|
5
|
+
|
|
6
|
+
// One command for "this instance is broken, why".
|
|
7
|
+
//
|
|
8
|
+
// It reads more than the obvious /incident, because several real failure shapes are
|
|
9
|
+
// invisible there:
|
|
10
|
+
// * A step that fails inside the caller's transaction rolls the transaction back, so no
|
|
11
|
+
// incident and no job survive; the only record is the historic job log, or nothing at
|
|
12
|
+
// all if the failure happened synchronously in an API call.
|
|
13
|
+
// * An incident carries both activityId (where the job hangs) and failedActivityId
|
|
14
|
+
// (where the error actually came from). Reporting only the first points at the wrong
|
|
15
|
+
// element, which is exactly what you do not want while hunting a bug.
|
|
16
|
+
export async function diagnoseCommand(id, options) {
|
|
17
|
+
const client = new Client(requireConfig());
|
|
18
|
+
|
|
19
|
+
const [historic, tree, incidents, jobs, jobLog, histIncidents, tasks, externalLog] = await Promise.all([
|
|
20
|
+
client.historyProcessInstance(id),
|
|
21
|
+
client.activityInstanceTree(id).catch(() => null),
|
|
22
|
+
client.incidents({ processInstanceId: id }).catch(() => []),
|
|
23
|
+
client.jobs({ processInstanceId: id }).catch(() => []),
|
|
24
|
+
client.historyJobLog({ processInstanceId: id, failureLog: true, sortBy: 'timestamp', sortOrder: 'desc', maxResults: 10 }).catch(() => []),
|
|
25
|
+
client.historyIncidents({ processInstanceId: id, sortBy: 'createTime', sortOrder: 'desc', maxResults: 10 }).catch(() => []),
|
|
26
|
+
client.tasks({ processInstanceId: id }).catch(() => []),
|
|
27
|
+
client.historyExternalTaskLog({ processInstanceId: id, failureLog: true, maxResults: 5 }).catch(() => []),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
if (out.isJsonMode()) {
|
|
31
|
+
return out.json({ instance: historic, activityTree: tree, incidents, jobs, jobLog, historicIncidents: histIncidents, tasks, externalTaskLog: externalLog });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
out.heading(`Instance ${id}`);
|
|
35
|
+
out.kv([
|
|
36
|
+
['definition', `${historic.processDefinitionKey} v${historic.processDefinitionVersion}`],
|
|
37
|
+
['state', out.stateLabel(historic.state)],
|
|
38
|
+
['started', out.formatDateTime(historic.startTime)],
|
|
39
|
+
['ended', historic.endTime ? out.formatDateTime(historic.endTime) : '-'],
|
|
40
|
+
['tenant', historic.tenantId ?? '-'],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const waiting = tree ? flatten(tree).filter((n) => n.activityType !== 'processDefinition') : [];
|
|
44
|
+
if (waiting.length > 0) {
|
|
45
|
+
out.line('\nStopped at');
|
|
46
|
+
out.table(null, waiting.map((n) => [` ${n.activityId}`, n.activityType, n.activityName ?? '']));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const problems = [];
|
|
50
|
+
|
|
51
|
+
for (const inc of incidents) {
|
|
52
|
+
problems.push({
|
|
53
|
+
source: 'open incident',
|
|
54
|
+
when: inc.incidentTimestamp,
|
|
55
|
+
activity: inc.failedActivityId || inc.activityId,
|
|
56
|
+
attachedAt: inc.activityId,
|
|
57
|
+
message: inc.incidentMessage,
|
|
58
|
+
jobId: inc.configuration,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const job of jobs.filter((j) => j.exceptionMessage)) {
|
|
63
|
+
if (incidents.some((i) => i.configuration === job.id)) continue;
|
|
64
|
+
problems.push({
|
|
65
|
+
source: `job (retries left ${job.retries})`,
|
|
66
|
+
when: job.createTime,
|
|
67
|
+
activity: job.failedActivityId,
|
|
68
|
+
message: job.exceptionMessage,
|
|
69
|
+
jobId: job.id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only surface the historic log when nothing live explains the failure, otherwise the
|
|
74
|
+
// same error is reported three times over.
|
|
75
|
+
if (problems.length === 0) {
|
|
76
|
+
for (const entry of jobLog) {
|
|
77
|
+
problems.push({
|
|
78
|
+
source: 'historic job failure',
|
|
79
|
+
when: entry.timestamp,
|
|
80
|
+
activity: entry.failedActivityId || entry.activityId,
|
|
81
|
+
attachedAt: entry.activityId,
|
|
82
|
+
message: entry.jobExceptionMessage,
|
|
83
|
+
historicJobLogId: entry.id,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const inc of histIncidents.filter((i) => i.endTime)) {
|
|
87
|
+
problems.push({
|
|
88
|
+
source: `resolved incident (${out.formatDateTime(inc.endTime)})`,
|
|
89
|
+
when: inc.createTime,
|
|
90
|
+
activity: inc.failedActivityId || inc.activityId,
|
|
91
|
+
message: inc.incidentMessage,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const entry of externalLog) {
|
|
97
|
+
problems.push({ source: 'external task failure', when: entry.timestamp, activity: entry.activityId, message: entry.errorMessage });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (problems.length === 0) {
|
|
101
|
+
if (historic.state === 'ACTIVE') {
|
|
102
|
+
out.line('\nNothing is failing. This instance is simply waiting.');
|
|
103
|
+
if (tasks.length > 0) {
|
|
104
|
+
out.table(null, tasks.map((t) => [` task ${t.id}`, out.truncate(t.name, 34), t.assignee ?? 'unassigned']));
|
|
105
|
+
out.note(`\nMove it along with: camunda complete <taskId> --var name=value`);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
out.line(`\nNothing is failing; the instance finished as ${historic.state}.`);
|
|
109
|
+
if (historic.state === 'EXTERNALLY_TERMINATED') {
|
|
110
|
+
out.note(`It was force-terminated${historic.deleteReason ? `: ${historic.deleteReason}` : ''}, not completed normally.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
out.line('');
|
|
117
|
+
out.problem(`${problems.length} problem(s)`);
|
|
118
|
+
for (const p of problems) {
|
|
119
|
+
out.line(`\n ${p.source} at ${out.formatDateTime(p.when)}`);
|
|
120
|
+
if (p.activity) out.line(` failing element: ${p.activity}`);
|
|
121
|
+
if (p.attachedAt && p.attachedAt !== p.activity) {
|
|
122
|
+
out.line(` job attached to: ${p.attachedAt} (the async marker sits here, the error came from the element above)`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const unwrapped = unwrapError(p.message);
|
|
126
|
+
out.line(` ${unwrapped.message}`);
|
|
127
|
+
|
|
128
|
+
for (const layer of unwrapped.layers) {
|
|
129
|
+
const text = typeof layer.value === 'string' ? layer.value : JSON.stringify(layer.value, null, 2);
|
|
130
|
+
out.note(` ${layer.label}:`);
|
|
131
|
+
for (const l of text.split('\n')) out.note(` ${l}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hint = explain(p.message);
|
|
135
|
+
if (hint) {
|
|
136
|
+
out.line('');
|
|
137
|
+
for (const l of out.wrap(hint, 92)) out.line(` ${l}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.stacktrace && (p.jobId || p.historicJobLogId)) {
|
|
141
|
+
try {
|
|
142
|
+
const trace = p.jobId ? await client.jobStacktrace(p.jobId) : await client.historyJobLogStacktrace(p.historicJobLogId);
|
|
143
|
+
const { header, frames, omitted } = condenseStacktrace(trace);
|
|
144
|
+
out.line('');
|
|
145
|
+
for (const h of header.slice(0, 3)) out.note(` ${h}`);
|
|
146
|
+
for (const f of frames) out.note(` ${f}`);
|
|
147
|
+
if (omitted > 0) out.note(` ... ${omitted} engine-internal frame(s) hidden`);
|
|
148
|
+
} catch {
|
|
149
|
+
/* the job may already be gone */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!options.stacktrace) out.note('\nAdd --stacktrace for the Java frames behind these.');
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The activity-instance tree nests scopes (a subprocess holds its children); flattening
|
|
159
|
+
// it gives the plain list of places a token is currently sitting.
|
|
160
|
+
function flatten(node, acc = []) {
|
|
161
|
+
acc.push(node);
|
|
162
|
+
for (const c of node.childActivityInstances ?? []) flatten(c, acc);
|
|
163
|
+
for (const t of node.childTransitionInstances ?? []) acc.push({ ...t, activityType: 'transition' });
|
|
164
|
+
return acc;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function incidentsCommand(options) {
|
|
168
|
+
const client = new Client(requireConfig());
|
|
169
|
+
const query = { maxResults: options.limit ?? 50 };
|
|
170
|
+
if (options.instance) query.processInstanceId = options.instance;
|
|
171
|
+
if (options.key) query.processDefinitionKeyIn = options.key;
|
|
172
|
+
if (options.tenant) query.tenantIdIn = options.tenant;
|
|
173
|
+
|
|
174
|
+
const rows = options.history
|
|
175
|
+
? await client.historyIncidents({ ...query, sortBy: 'createTime', sortOrder: 'desc' })
|
|
176
|
+
: await client.incidents(query);
|
|
177
|
+
|
|
178
|
+
if (out.isJsonMode()) return out.json(rows);
|
|
179
|
+
if (rows.length === 0) return out.note(options.history ? 'No incidents recorded.' : 'No open incidents.');
|
|
180
|
+
|
|
181
|
+
out.table(
|
|
182
|
+
['INSTANCE', 'FAILING ELEMENT', 'TYPE', 'SINCE', 'MESSAGE'],
|
|
183
|
+
rows.map((i) => [
|
|
184
|
+
i.processInstanceId,
|
|
185
|
+
i.failedActivityId || i.activityId || '-',
|
|
186
|
+
i.incidentType,
|
|
187
|
+
out.formatDateTime(i.incidentTimestamp || i.createTime),
|
|
188
|
+
out.truncate(unwrapError(i.incidentMessage).message, 60),
|
|
189
|
+
])
|
|
190
|
+
);
|
|
191
|
+
out.note(`\n${rows.length} incident(s). Detail: camunda diagnose <instanceId>`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function jobsCommand(options) {
|
|
195
|
+
const client = new Client(requireConfig());
|
|
196
|
+
const query = { maxResults: options.limit ?? 50 };
|
|
197
|
+
if (options.instance) query.processInstanceId = options.instance;
|
|
198
|
+
if (options.key) query.processDefinitionKey = options.key;
|
|
199
|
+
if (options.failed) query.withException = true;
|
|
200
|
+
|
|
201
|
+
const jobs = await client.jobs(query);
|
|
202
|
+
if (out.isJsonMode()) return out.json(jobs);
|
|
203
|
+
if (jobs.length === 0) return out.note('No jobs match.');
|
|
204
|
+
|
|
205
|
+
out.table(
|
|
206
|
+
['ID', 'INSTANCE', 'ELEMENT', 'RETRIES', 'DUE', 'ERROR'],
|
|
207
|
+
jobs.map((j) => [
|
|
208
|
+
j.id,
|
|
209
|
+
j.processInstanceId,
|
|
210
|
+
j.failedActivityId || '-',
|
|
211
|
+
String(j.retries),
|
|
212
|
+
out.formatDateTime(j.dueDate),
|
|
213
|
+
out.truncate(unwrapError(j.exceptionMessage || '').message, 50),
|
|
214
|
+
])
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function stacktraceCommand(jobId, options) {
|
|
219
|
+
const client = new Client(requireConfig());
|
|
220
|
+
const trace = await client.jobStacktrace(jobId);
|
|
221
|
+
if (options.full) return console.log(trace);
|
|
222
|
+
const { header, frames, omitted } = condenseStacktrace(trace, { keep: 20 });
|
|
223
|
+
for (const h of header) out.line(h);
|
|
224
|
+
for (const f of frames) out.note(` ${f}`);
|
|
225
|
+
if (omitted > 0) out.note(` ... ${omitted} engine-internal frame(s) hidden, use --full for everything`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The execution trace. Ordering matters here: several activities routinely share a
|
|
229
|
+
// millisecond, so ties are broken by following the sequence flows rather than trusting
|
|
230
|
+
// the timestamp alone.
|
|
231
|
+
export async function traceCommand(id, options) {
|
|
232
|
+
const client = new Client(requireConfig());
|
|
233
|
+
const activities = await client.historyActivityInstances({
|
|
234
|
+
processInstanceId: id,
|
|
235
|
+
maxResults: options.limit ?? 200,
|
|
236
|
+
sortBy: 'startTime',
|
|
237
|
+
sortOrder: 'asc',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (out.isJsonMode()) return out.json(activities);
|
|
241
|
+
if (activities.length === 0) return out.note('No activity recorded for this instance.');
|
|
242
|
+
|
|
243
|
+
const rows = activities.map((a) => [
|
|
244
|
+
out.formatDateTime(a.startTime),
|
|
245
|
+
a.activityType,
|
|
246
|
+
a.activityId,
|
|
247
|
+
out.truncate(a.activityName ?? '', 32),
|
|
248
|
+
out.formatDuration(a.durationInMillis),
|
|
249
|
+
a.canceled ? 'canceled' : a.endTime ? '' : 'running',
|
|
250
|
+
]);
|
|
251
|
+
out.table(['TIME', 'TYPE', 'ID', 'NAME', 'TOOK', ''], rows);
|
|
252
|
+
|
|
253
|
+
const running = activities.filter((a) => !a.endTime);
|
|
254
|
+
if (running.length > 0) {
|
|
255
|
+
out.note(`\nStill open: ${running.map((a) => a.activityId).join(', ')}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { Client, parseVariableFlags, resolveDefinition } from '../client.js';
|
|
4
|
+
import { requireConfig } from '../config.js';
|
|
5
|
+
import * as out from '../output.js';
|
|
6
|
+
|
|
7
|
+
export async function instancesCommand(options) {
|
|
8
|
+
const client = new Client(requireConfig());
|
|
9
|
+
|
|
10
|
+
if (options.history) {
|
|
11
|
+
const query = { maxResults: options.limit ?? 30, sortBy: 'startTime', sortOrder: 'desc' };
|
|
12
|
+
if (options.key) query.processDefinitionKey = options.key;
|
|
13
|
+
if (options.tenant) query.tenantIdIn = options.tenant;
|
|
14
|
+
if (options.businessKey) query.processInstanceBusinessKey = options.businessKey;
|
|
15
|
+
if (options.state) query[options.state] = true;
|
|
16
|
+
|
|
17
|
+
const rows = await client.historyProcessInstances(query);
|
|
18
|
+
if (out.isJsonMode()) return out.json(rows);
|
|
19
|
+
if (rows.length === 0) return out.note('No instances match.');
|
|
20
|
+
out.table(
|
|
21
|
+
['ID', 'DEFINITION', 'STARTED', 'DURATION', 'STATE'],
|
|
22
|
+
rows.map((i) => [
|
|
23
|
+
i.id,
|
|
24
|
+
out.truncate(i.processDefinitionKey, 34),
|
|
25
|
+
out.formatDateTime(i.startTime),
|
|
26
|
+
out.formatDuration(i.durationInMillis),
|
|
27
|
+
out.stateLabel(i.state),
|
|
28
|
+
])
|
|
29
|
+
);
|
|
30
|
+
return out.note(`\n${rows.length} instance(s), history`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const query = { maxResults: options.limit ?? 50 };
|
|
34
|
+
if (options.key) query.processDefinitionKey = options.key;
|
|
35
|
+
if (options.businessKey) query.businessKey = options.businessKey;
|
|
36
|
+
if (options.tenant) query.tenantIdIn = options.tenant;
|
|
37
|
+
if (options.withIncident) query.withIncident = true;
|
|
38
|
+
|
|
39
|
+
const instances = await client.processInstances(query);
|
|
40
|
+
if (out.isJsonMode()) return out.json(instances);
|
|
41
|
+
if (instances.length === 0) return out.note('No running instances match.');
|
|
42
|
+
|
|
43
|
+
out.table(
|
|
44
|
+
['ID', 'DEFINITION', 'BUSINESS KEY', 'TENANT'],
|
|
45
|
+
instances.map((i) => [i.id, i.definitionId.split(':')[0], i.businessKey ?? '-', i.tenantId ?? '-'])
|
|
46
|
+
);
|
|
47
|
+
out.note(`\n${instances.length} running instance(s)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Shows where the instance is now rather than only what it is. The activity-instance
|
|
51
|
+
// tree is the live token position; history alone cannot tell you that.
|
|
52
|
+
export async function instanceCommand(id, options) {
|
|
53
|
+
const client = new Client(requireConfig());
|
|
54
|
+
|
|
55
|
+
const [historic, variables, tree, incidents, tasks] = await Promise.all([
|
|
56
|
+
client.historyProcessInstance(id),
|
|
57
|
+
client.historyVariableInstances({ processInstanceId: id, maxResults: 200 }),
|
|
58
|
+
client.activityInstanceTree(id).catch(() => null),
|
|
59
|
+
client.incidents({ processInstanceId: id }).catch(() => []),
|
|
60
|
+
client.tasks({ processInstanceId: id }).catch(() => []),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
if (out.isJsonMode()) return out.json({ instance: historic, variables, activityTree: tree, incidents, tasks });
|
|
64
|
+
|
|
65
|
+
out.heading(`Instance ${historic.id}`);
|
|
66
|
+
out.kv([
|
|
67
|
+
['definition', `${historic.processDefinitionKey} v${historic.processDefinitionVersion}`],
|
|
68
|
+
['state', out.stateLabel(historic.state)],
|
|
69
|
+
['business key', historic.businessKey ?? '-'],
|
|
70
|
+
['tenant', historic.tenantId ?? '-'],
|
|
71
|
+
['started', out.formatDateTime(historic.startTime)],
|
|
72
|
+
['ended', historic.endTime ? out.formatDateTime(historic.endTime) : '-'],
|
|
73
|
+
['duration', out.formatDuration(historic.durationInMillis)],
|
|
74
|
+
['started by', historic.startUserId ?? '-'],
|
|
75
|
+
['delete reason', historic.deleteReason ?? ''],
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const waiting = tree ? flattenTree(tree).filter((n) => n.activityType !== 'processDefinition') : [];
|
|
79
|
+
if (waiting.length > 0) {
|
|
80
|
+
out.line('\nCurrently at');
|
|
81
|
+
out.table(
|
|
82
|
+
null,
|
|
83
|
+
waiting.map((n) => [` ${n.activityId}`, n.activityType, n.activityName ?? '', (n.incidentIds ?? []).length ? 'has incident' : ''])
|
|
84
|
+
);
|
|
85
|
+
} else if (historic.state === 'ACTIVE') {
|
|
86
|
+
out.note('\nActive but not sitting at any activity (it may be between transitions).');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (tasks.length > 0) {
|
|
90
|
+
out.line('\nOpen tasks');
|
|
91
|
+
out.table(null, tasks.map((t) => [` ${t.id}`, out.truncate(t.name, 34), t.assignee ?? 'unassigned']));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (incidents.length > 0) {
|
|
95
|
+
out.line('');
|
|
96
|
+
out.problem(`${incidents.length} open incident(s). Run: camunda diagnose ${id}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (variables.length > 0) {
|
|
100
|
+
out.line('\nVariables');
|
|
101
|
+
out.table(
|
|
102
|
+
null,
|
|
103
|
+
variables.map((v) => [` ${v.name}`, v.type, out.truncate(JSON.stringify(v.value), 70)])
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
out.note('\nNo variables set on this instance.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function startCommand(keyOrId, options) {
|
|
111
|
+
const client = new Client(requireConfig());
|
|
112
|
+
const def = await resolveDefinition(client, keyOrId, options);
|
|
113
|
+
const variables = parseVariableFlags(options.var);
|
|
114
|
+
|
|
115
|
+
const result = await client.startByDefinitionId(def.id, {
|
|
116
|
+
businessKey: options.businessKey,
|
|
117
|
+
variables,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// A start returning 200 only means the engine accepted it. Anything marked async runs
|
|
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 });
|
|
134
|
+
|
|
135
|
+
out.line(`Started ${def.key} v${def.version} as instance ${result.id}`);
|
|
136
|
+
if (result.businessKey) out.note(`business key ${result.businessKey}`);
|
|
137
|
+
|
|
138
|
+
if (trouble.incidents.length > 0 || trouble.jobs.length > 0) {
|
|
139
|
+
const msg = trouble.incidents[0]?.incidentMessage || trouble.jobs[0]?.exceptionMessage;
|
|
140
|
+
out.problem(`\nIt already failed after starting: ${msg}`);
|
|
141
|
+
out.note(`Run: camunda diagnose ${result.id}`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function cancelCommand(id, options) {
|
|
147
|
+
const client = new Client(requireConfig());
|
|
148
|
+
|
|
149
|
+
if (!options.yes) {
|
|
150
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
151
|
+
const answer = await rl.question(
|
|
152
|
+
`Force-terminating ${id} cannot be undone (it ends as EXTERNALLY_TERMINATED, not COMPLETED).\nType the instance id to confirm: `
|
|
153
|
+
);
|
|
154
|
+
rl.close();
|
|
155
|
+
if (answer.trim() !== id) return out.note('Aborted, the id did not match.');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await client.deleteProcessInstance(id, { reason: options.reason });
|
|
159
|
+
out.line(`Instance ${id} terminated.`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function varsCommand(id, options) {
|
|
163
|
+
const client = new Client(requireConfig());
|
|
164
|
+
|
|
165
|
+
if (options.history) {
|
|
166
|
+
const details = await client.historyDetails({
|
|
167
|
+
processInstanceId: id,
|
|
168
|
+
type: 'variableUpdate',
|
|
169
|
+
maxResults: options.limit ?? 200,
|
|
170
|
+
sortBy: 'time',
|
|
171
|
+
sortOrder: 'asc',
|
|
172
|
+
});
|
|
173
|
+
if (out.isJsonMode()) return out.json(details);
|
|
174
|
+
if (details.length === 0) {
|
|
175
|
+
out.note('No variable updates recorded. This needs the engine history level at FULL.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
out.table(
|
|
179
|
+
['TIME', 'VARIABLE', 'TYPE', 'REV', 'VALUE'],
|
|
180
|
+
details.map((d) => [
|
|
181
|
+
out.formatDateTime(d.time),
|
|
182
|
+
d.variableName,
|
|
183
|
+
d.variableType,
|
|
184
|
+
String(d.revision ?? ''),
|
|
185
|
+
out.truncate(JSON.stringify(d.value), 50),
|
|
186
|
+
])
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const variables = await client.historyVariableInstances({ processInstanceId: id, maxResults: 500 });
|
|
192
|
+
if (out.isJsonMode()) return out.json(variables);
|
|
193
|
+
if (variables.length === 0) return out.note('No variables on this instance.');
|
|
194
|
+
out.table(
|
|
195
|
+
['NAME', 'TYPE', 'VALUE'],
|
|
196
|
+
variables.map((v) => [v.name, v.type, out.truncate(JSON.stringify(v.value), 80)])
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function flattenTree(node, acc = []) {
|
|
201
|
+
acc.push(node);
|
|
202
|
+
for (const c of node.childActivityInstances ?? []) flattenTree(c, acc);
|
|
203
|
+
for (const t of node.childTransitionInstances ?? []) acc.push({ ...t, activityType: 'transition' });
|
|
204
|
+
return acc;
|
|
205
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Client } from '../client.js';
|
|
2
|
+
import { requireConfig } from '../config.js';
|
|
3
|
+
import { unwrapError } from '../errors.js';
|
|
4
|
+
import * as out from '../output.js';
|
|
5
|
+
|
|
6
|
+
// Repair operations. A job that ran out of retries stays put until its retry count is
|
|
7
|
+
// raised, which is how you re-run a step after fixing the addon behind it without
|
|
8
|
+
// starting the whole instance over.
|
|
9
|
+
export async function retryCommand(jobId, options) {
|
|
10
|
+
const client = new Client(requireConfig());
|
|
11
|
+
const retries = options.retries ?? 1;
|
|
12
|
+
await client.setJobRetries(jobId, retries);
|
|
13
|
+
out.line(`Job ${jobId} set to ${retries} retry/retries.`);
|
|
14
|
+
|
|
15
|
+
if (options.now) {
|
|
16
|
+
try {
|
|
17
|
+
await client.executeJob(jobId);
|
|
18
|
+
out.line('Executed immediately, it succeeded.');
|
|
19
|
+
} catch (err) {
|
|
20
|
+
const unwrapped = unwrapError(err.body?.message || err.message);
|
|
21
|
+
out.problem(`Ran it now and it failed again: ${unwrapped.message}`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
out.note('The job runner will pick it up shortly. Use --now to run it immediately instead.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Runs a pending job at once rather than waiting for the scheduler. Useful for testing a
|
|
30
|
+
// step marked async without sitting through the poll interval.
|
|
31
|
+
export async function runJobCommand(jobId) {
|
|
32
|
+
const client = new Client(requireConfig());
|
|
33
|
+
try {
|
|
34
|
+
await client.executeJob(jobId);
|
|
35
|
+
out.line(`Job ${jobId} executed.`);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const unwrapped = unwrapError(err.body?.message || err.message);
|
|
38
|
+
out.problem(`Job ${jobId} failed: ${unwrapped.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function setVarCommand(instanceId, assignment, options) {
|
|
44
|
+
const client = new Client(requireConfig());
|
|
45
|
+
const eq = assignment.indexOf('=');
|
|
46
|
+
if (eq === -1) throw new Error('Expected name=value.');
|
|
47
|
+
const name = assignment.slice(0, eq);
|
|
48
|
+
const raw = assignment.slice(eq + 1);
|
|
49
|
+
const type = options.type ?? 'String';
|
|
50
|
+
|
|
51
|
+
let value = raw;
|
|
52
|
+
if (type === 'Integer' || type === 'Long') value = parseInt(raw, 10);
|
|
53
|
+
else if (type === 'Double') value = parseFloat(raw);
|
|
54
|
+
else if (type === 'Boolean') value = raw === 'true';
|
|
55
|
+
else if (type === 'Json') value = JSON.parse(raw);
|
|
56
|
+
|
|
57
|
+
await client.setProcessInstanceVariable(instanceId, name, { value, type });
|
|
58
|
+
out.line(`Set ${name} (${type}) on instance ${instanceId}.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function messageCommand(name, options) {
|
|
62
|
+
const client = new Client(requireConfig());
|
|
63
|
+
const body = { messageName: name, resultEnabled: true };
|
|
64
|
+
if (options.instance) body.processInstanceId = options.instance;
|
|
65
|
+
if (options.businessKey) body.businessKey = options.businessKey;
|
|
66
|
+
if (options.all) body.all = true;
|
|
67
|
+
|
|
68
|
+
const result = await client.correlateMessage(body);
|
|
69
|
+
if (out.isJsonMode()) return out.json(result);
|
|
70
|
+
|
|
71
|
+
if (!result || result.length === 0) {
|
|
72
|
+
out.warn(`Message "${name}" correlated with nothing. No instance is waiting for it.`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const r of result) {
|
|
77
|
+
out.line(`Correlated with ${r.resultType === 'ProcessDefinition' ? 'new instance' : 'instance'} ${r.processInstance?.id ?? r.execution?.processInstanceId}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function subscriptionsCommand(options) {
|
|
82
|
+
const client = new Client(requireConfig());
|
|
83
|
+
const query = { maxResults: options.limit ?? 50 };
|
|
84
|
+
if (options.instance) query.processInstanceId = options.instance;
|
|
85
|
+
if (options.type) query.eventType = options.type;
|
|
86
|
+
|
|
87
|
+
const subs = await client.eventSubscriptions(query);
|
|
88
|
+
if (out.isJsonMode()) return out.json(subs);
|
|
89
|
+
if (subs.length === 0) return out.note('Nothing is waiting on a message or signal.');
|
|
90
|
+
|
|
91
|
+
out.table(
|
|
92
|
+
['TYPE', 'NAME', 'ELEMENT', 'INSTANCE', 'SINCE'],
|
|
93
|
+
subs.map((s) => [s.eventType, s.eventName ?? '-', s.activityId ?? '-', s.processInstanceId ?? '-', out.formatDateTime(s.createdDate)])
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function usersCommand(options) {
|
|
98
|
+
const client = new Client(requireConfig());
|
|
99
|
+
const query = { maxResults: options.limit ?? 50 };
|
|
100
|
+
if (options.search) query.idLike = `%${options.search}%`;
|
|
101
|
+
const users = await client.users(query);
|
|
102
|
+
if (out.isJsonMode()) return out.json(users);
|
|
103
|
+
if (users.length === 0) return out.note('No users found.');
|
|
104
|
+
out.table(
|
|
105
|
+
['ID', 'FIRST', 'LAST', 'EMAIL'],
|
|
106
|
+
users.map((u) => [u.id, u.firstName ?? '', u.lastName ?? '', u.email ?? ''])
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function groupsCommand(options) {
|
|
111
|
+
const client = new Client(requireConfig());
|
|
112
|
+
const groups = await client.groups({ maxResults: options.limit ?? 50 });
|
|
113
|
+
if (out.isJsonMode()) return out.json(groups);
|
|
114
|
+
if (groups.length === 0) return out.note('No groups found.');
|
|
115
|
+
out.table(['ID', 'NAME', 'TYPE'], groups.map((g) => [g.id, g.name ?? '', g.type ?? '']));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function tenantsCommand(options) {
|
|
119
|
+
const client = new Client(requireConfig());
|
|
120
|
+
const query = { maxResults: options.limit ?? 200 };
|
|
121
|
+
if (options.search) query.nameLike = `%${options.search}%`;
|
|
122
|
+
const tenants = await client.tenants(query);
|
|
123
|
+
if (out.isJsonMode()) return out.json(tenants);
|
|
124
|
+
if (tenants.length === 0) return out.note('No tenants found.');
|
|
125
|
+
out.table(['ID', 'NAME'], tenants.map((t) => [t.id, t.name ?? '']));
|
|
126
|
+
out.note(`\n${tenants.length} tenant(s)`);
|
|
127
|
+
}
|