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 ADDED
@@ -0,0 +1,167 @@
1
+ # camunda-cli
2
+
3
+ Command-line client for **self-hosted Camunda 7**, aimed at the two things that take the
4
+ most time when developing BPMN: understanding a model that is already deployed, and working
5
+ out why an instance is stuck. Not affiliated with Camunda GmbH.
6
+
7
+ Built and verified against Camunda 7.24 on a live multi-tenant deployment.
8
+
9
+ ```
10
+ $ camunda lint order-process
11
+
12
+ ERROR uncovered-value Gateway_1jk90v6
13
+ "amount check" branches on amount > 300 and amount < 300, so amount == 300 matches
14
+ neither branch and the instance will fail there.
15
+
16
+ ERROR variable-name-mismatch flow Flow_0jpsuir (retry)
17
+ Reads "input_huruf", which nothing in this process writes, while a form here writes
18
+ "input_9ltnt". These names look related, so this is most likely a typo: the
19
+ expression throws "Cannot resolve identifier 'input_huruf'" the moment it is evaluated.
20
+ ```
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g camunda-cli
26
+ camunda login https://your-host/engine-rest
27
+ ```
28
+
29
+ `login` prompts for a username and password and stores them in
30
+ `~/.config/camunda-cli/config.json` with mode `0600`. Camunda 7's REST API authenticates
31
+ with HTTP Basic on every request and has no token to exchange, so the password is kept
32
+ rather than a session. It is sent only to the engine you configured.
33
+
34
+ Installing globally without root:
35
+
36
+ ```bash
37
+ npm config set prefix ~/.npm-global
38
+ export PATH="$HOME/.npm-global/bin:$PATH"
39
+ npm install -g camunda-cli
40
+ ```
41
+
42
+ ## What it is for
43
+
44
+ **Read a deployed model.** Much of what you need while debugging — which variable a gateway
45
+ condition reads, whether a step is async, what a service task is actually wired to — is only
46
+ in the deployed XML, not behind any REST resource. `inspect` reads it out and lays it flat.
47
+
48
+ ```bash
49
+ camunda inspect order-process
50
+ ```
51
+
52
+ ```
53
+ Flow nodes
54
+ TYPE ID NAME LIVE DETAIL
55
+ startEvent StartEvent_1 order received 1 field(s)
56
+ userTask Activity_11j96l5 check stock [3 here] assignee=ops 2 field(s)
57
+ serviceTask Activity_0dfmi5x charge card async:before addon=payments/charge#98
58
+ exclusiveGateway Gateway_1jk90v6 paid?
59
+
60
+ Sequence flows
61
+ FROM TO LABEL CONDITION
62
+ Gateway_1jk90v6 Event_0ufajup yes ${paid == true}
63
+ Gateway_1jk90v6 Activity_retry no ${paid == false}
64
+ ```
65
+
66
+ **Find the bugs before an instance does.** `lint` checks the deployed model statically.
67
+ Every rule exists because that failure was reproduced against a real engine first:
68
+
69
+ | Rule | What it catches |
70
+ |---|---|
71
+ | `uncovered-value` | `> N` and `< N` branches that leave `== N` with nowhere to go (`ENGINE-02004`) |
72
+ | `no-default-flow` | Every branch conditional, no default: any instance where they all fail stops dead |
73
+ | `variable-name-mismatch` | A condition reads `foo` while the form writes `foo_2`, so the expression throws |
74
+ | `unwritten-variable` | A direct `${x}` read where nothing sets `x`; throws instead of yielding null |
75
+ | `initiator-expression` | `${initiator}`, which exists only when started through AlurKerja's API |
76
+ | `no-op-service-task` | A service task with no implementation behind it |
77
+ | `addon-without-config` | An integration call with no config bound |
78
+ | `unreachable`, `dead-end`, `dangling-flow` | Structural mistakes |
79
+
80
+ `deploy` runs the same checks and refuses to push a model with blocking issues, unless you
81
+ pass `--skip-lint`.
82
+
83
+ **Work out why an instance is stuck.** `diagnose` gathers what is scattered across several
84
+ endpoints and unpacks it:
85
+
86
+ ```bash
87
+ camunda diagnose 3426894
88
+ ```
89
+
90
+ ```
91
+ Stopped at
92
+ Activity_CekSisaKuota transition Check Remaining Quota
93
+
94
+ 1 problem(s)
95
+
96
+ open incident at 2026-08-13 08:19:33
97
+ failing element: Activity_TinjauPengajuan
98
+ job attached to: Activity_CekSisaKuota (the async marker sits here, the error came from
99
+ the element above)
100
+ Unknown property used in expression: ${initiator}. Cause: Cannot resolve identifier 'initiator'
101
+ ```
102
+
103
+ It reads more than `/incident`, because several real failures are invisible there. A step
104
+ that fails inside the caller's transaction leaves no incident and no job behind at all; an
105
+ incident points at the activity holding the job, which is often not the activity that
106
+ failed; and a resolved incident disappears from `/incident` entirely.
107
+
108
+ It also unpacks nested integration errors. An addon failure arrives as a REST message
109
+ wrapping a Java exception wrapping a JSON body whose `output` field is itself JSON. The
110
+ sentence you need is at the bottom, so that is what gets printed first.
111
+
112
+ ## Commands
113
+
114
+ ```
115
+ Session login logout whoami
116
+ Models definitions inspect lint xml stats
117
+ Instances instances instance start cancel vars set-var
118
+ Diagnosis diagnose trace incidents jobs stacktrace
119
+ Tasks tasks task complete claim
120
+ Deployment deployments deploy undeploy
121
+ Events message subscriptions
122
+ Repair retry run-job
123
+ Identity users groups tenants
124
+ ```
125
+
126
+ `camunda <command> --help` for the options on any of them.
127
+
128
+ ## Notes that save time
129
+
130
+ **Tenants.** On a shared engine the same process key exists under many tenants, and the
131
+ engine's key-based endpoints answer *"no matching process definition ... and no tenant-id"*,
132
+ which reads like the process is missing when it is not. Every command taking a key accepts
133
+ `--tenant`, and an ambiguous key lists the candidates rather than guessing.
134
+
135
+ **`start` waits before reporting success.** A start returning HTTP 200 only means the engine
136
+ accepted it; anything marked async runs after the response. `start` looks for a failure a
137
+ moment later and reports it, instead of leaving you with a green message and a broken
138
+ instance. `--no-wait` skips that.
139
+
140
+ **Variable types matter.** `--var n=300` sends a string, and `"300" > 200` is a string
141
+ comparison. Use `--var n=300:Integer` where a gateway compares numerically, or `name:=<json>`
142
+ for structured values.
143
+
144
+ **Transient 401s are retried.** A load-balanced engine will intermittently reject a valid
145
+ credential while a replica is unhealthy: five consecutive 401s followed by five 200s, same
146
+ credential, seconds apart, is a real pattern observed in production. Requests are retried so
147
+ this does not get misread as a wrong password. A 4xx carrying a real Camunda error body is
148
+ never retried.
149
+
150
+ **Output is meant to be piped.** No box-drawing characters, and no colour unless stdout is a
151
+ terminal. Every command takes `--json` for the raw API payload.
152
+
153
+ ## Requirements
154
+
155
+ - Node.js 18+
156
+ - A Camunda 7 engine with its REST API enabled
157
+
158
+ ## Not covered
159
+
160
+ DMN evaluation, batch operations, instance migration and modification, and authorization
161
+ management. These are deliberate omissions rather than oversights: the command surface here
162
+ covers what came up repeatedly while developing and debugging processes, not all 300-odd
163
+ REST endpoints. Pull requests welcome.
164
+
165
+ ## License
166
+
167
+ MIT
package/bin/camunda.js ADDED
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { configureOutput, isJsonMode } from '../src/output.js';
4
+ import { unwrapError, explain } from '../src/errors.js';
5
+ import { loginCommand, logoutCommand, whoamiCommand } from '../src/commands/session.js';
6
+ import { definitionsCommand, inspectCommand, lintCommand, xmlCommand, statsCommand } from '../src/commands/definitions.js';
7
+ import { instancesCommand, instanceCommand, startCommand, cancelCommand, varsCommand } from '../src/commands/instances.js';
8
+ import { diagnoseCommand, incidentsCommand, jobsCommand, stacktraceCommand, traceCommand } from '../src/commands/diagnose.js';
9
+ import { tasksCommand, taskCommand, completeCommand, claimCommand } from '../src/commands/tasks.js';
10
+ import { deploymentsCommand, deployCommand, undeployCommand } from '../src/commands/deploy.js';
11
+ import {
12
+ retryCommand, runJobCommand, setVarCommand, messageCommand, subscriptionsCommand,
13
+ usersCommand, groupsCommand, tenantsCommand,
14
+ } from '../src/commands/ops.js';
15
+
16
+ const program = new Command();
17
+ const int = (v) => parseInt(v, 10);
18
+ const collect = (v, prev) => [...prev, v];
19
+
20
+ program
21
+ .name('camunda')
22
+ .description(
23
+ 'Command-line client for self-hosted Camunda 7.\n\n' +
24
+ 'Start with "camunda inspect <key>" to read a deployed model, and\n' +
25
+ '"camunda diagnose <instanceId>" when an instance misbehaves.'
26
+ )
27
+ .version('0.2.0')
28
+ .option('--json', 'print the raw API payload instead of a formatted view')
29
+ .option('--no-color', 'never emit colour, even on a terminal')
30
+ .showHelpAfterError()
31
+ .hook('preAction', (thisCommand) => {
32
+ const opts = thisCommand.opts();
33
+ configureOutput({ json: opts.json, color: opts.color });
34
+ });
35
+
36
+ // Shared option shapes. Multi-tenancy is not optional in practice: the same process key
37
+ // lives under many tenants in a shared engine, and every key-based lookup needs it.
38
+ const withTenant = (cmd) => cmd.option('-t, --tenant <tenantId>', 'restrict to one tenant');
39
+ const withVersion = (cmd) => cmd.option('--pd-version <n>', 'a specific definition version instead of the latest', int);
40
+
41
+ // --- session ---------------------------------------------------------------
42
+ program
43
+ .command('login <url>')
44
+ .description('Store credentials for an engine (the URL may omit /engine-rest)')
45
+ .option('-u, --username <username>', 'prompted for if omitted')
46
+ .option('-p, --password <password>', 'prompted for if omitted, which keeps it out of your shell history')
47
+ .option('--no-suffix', 'use the URL exactly as given')
48
+ .action(loginCommand);
49
+
50
+ program.command('logout').description('Forget the stored credentials').action(logoutCommand);
51
+ program.command('whoami').description('Which engine is configured, and its current totals').action(whoamiCommand);
52
+
53
+ // --- reading models --------------------------------------------------------
54
+ withTenant(
55
+ program
56
+ .command('definitions')
57
+ .alias('defs')
58
+ .description('List deployed process definitions')
59
+ .option('-k, --key <substring>', 'match the definition key')
60
+ .option('-n, --name <substring>', 'match the definition name')
61
+ .option('-l, --latest', 'only the newest version of each')
62
+ .option('--limit <n>', 'maximum rows', int)
63
+ ).action(definitionsCommand);
64
+
65
+ withVersion(
66
+ withTenant(
67
+ program
68
+ .command('inspect <keyOrId>')
69
+ .description('Structure of a deployed model: elements, flow conditions, forms, integrations')
70
+ .option('--no-lint', 'skip the static-check summary at the end')
71
+ )
72
+ ).action(inspectCommand);
73
+
74
+ withVersion(
75
+ withTenant(
76
+ program
77
+ .command('lint <keyOrId>')
78
+ .description('Static checks over a deployed model, before an instance has to find the bugs')
79
+ .option('-a, --all', 'include informational findings')
80
+ .addOption(new Option('-s, --severity <level>', 'only one severity').choices(['error', 'warning', 'info']))
81
+ )
82
+ ).action(lintCommand);
83
+
84
+ withVersion(
85
+ withTenant(program.command('xml <keyOrId>').description('The deployed BPMN XML').option('-o, --output <file>', 'write to a file'))
86
+ ).action(xmlCommand);
87
+
88
+ withVersion(
89
+ withTenant(program.command('stats <keyOrId>').description('How many instances sit at each element, and what is failing there'))
90
+ ).action(statsCommand);
91
+
92
+ // --- instances -------------------------------------------------------------
93
+ withTenant(
94
+ program
95
+ .command('instances')
96
+ .alias('ps')
97
+ .description('Running instances, or finished ones with --history')
98
+ .option('-k, --key <definitionKey>')
99
+ .option('-b, --business-key <key>')
100
+ .option('-i, --with-incident', 'only instances that have an incident')
101
+ .option('-H, --history', 'search history instead of running instances')
102
+ .addOption(new Option('--state <state>', 'with --history').choices(['finished', 'unfinished', 'completed', 'externallyTerminated', 'internallyTerminated']))
103
+ .option('--limit <n>', 'maximum rows', int)
104
+ ).action(instancesCommand);
105
+
106
+ program
107
+ .command('instance <id>')
108
+ .description('One instance: state, where it currently sits, tasks, variables')
109
+ .action(instanceCommand);
110
+
111
+ withVersion(
112
+ withTenant(
113
+ program
114
+ .command('start <keyOrId>')
115
+ .description('Start an instance, then check whether it failed immediately afterwards')
116
+ .option('-b, --business-key <key>')
117
+ .option('--var <name=value>', 'repeatable; name=value, name=value:Type, or name:=<json>', collect, [])
118
+ .option('--no-wait', 'do not look for asynchronous failures after starting')
119
+ .option('--wait <ms>', 'how long to wait before that check', int)
120
+ )
121
+ ).action(startCommand);
122
+
123
+ program
124
+ .command('cancel <id>')
125
+ .description('Force-terminate a running instance; it ends as EXTERNALLY_TERMINATED')
126
+ .option('-r, --reason <text>', 'recorded against the instance')
127
+ .option('-y, --yes', 'skip the confirmation prompt')
128
+ .action(cancelCommand);
129
+
130
+ program
131
+ .command('vars <instanceId>')
132
+ .description('Variables on an instance, or every write to them with --history')
133
+ .option('-H, --history', 'each individual update, oldest first')
134
+ .option('--limit <n>', 'maximum rows', int)
135
+ .action(varsCommand);
136
+
137
+ program
138
+ .command('set-var <instanceId> <name=value>')
139
+ .description('Overwrite a variable on a running instance')
140
+ .addOption(new Option('--type <type>').choices(['String', 'Integer', 'Long', 'Double', 'Boolean', 'Json']).default('String'))
141
+ .action(setVarCommand);
142
+
143
+ // --- diagnosis -------------------------------------------------------------
144
+ program
145
+ .command('diagnose <instanceId>')
146
+ .alias('why')
147
+ .description('Why an instance is stuck or failed, with the nested integration errors unpacked')
148
+ .option('--stacktrace', 'include the Java frames behind each failure')
149
+ .action(diagnoseCommand);
150
+
151
+ program
152
+ .command('trace <instanceId>')
153
+ .description('Every activity this instance has been through, in order')
154
+ .option('--limit <n>', 'maximum rows', int)
155
+ .action(traceCommand);
156
+
157
+ withTenant(
158
+ program
159
+ .command('incidents')
160
+ .description('Open incidents, or every recorded one with --history')
161
+ .option('-i, --instance <processInstanceId>')
162
+ .option('-k, --key <definitionKey>')
163
+ .option('-H, --history', 'include incidents that were already resolved')
164
+ .option('--limit <n>', 'maximum rows', int)
165
+ ).action(incidentsCommand);
166
+
167
+ program
168
+ .command('jobs')
169
+ .description('Pending and failed jobs')
170
+ .option('-i, --instance <processInstanceId>')
171
+ .option('-k, --key <definitionKey>')
172
+ .option('-f, --failed', 'only jobs carrying an exception')
173
+ .option('--limit <n>', 'maximum rows', int)
174
+ .action(jobsCommand);
175
+
176
+ program
177
+ .command('stacktrace <jobId>')
178
+ .description('The stack trace behind a failed job, engine internals filtered out')
179
+ .option('--full', 'every frame, unfiltered')
180
+ .action(stacktraceCommand);
181
+
182
+ // --- tasks -----------------------------------------------------------------
183
+ withTenant(
184
+ program
185
+ .command('tasks')
186
+ .description('Open human tasks')
187
+ .option('-a, --assignee <userId>')
188
+ .option('-i, --instance <processInstanceId>')
189
+ .option('-k, --key <definitionKey>')
190
+ .option('-u, --unassigned')
191
+ .option('--limit <n>', 'maximum rows', int)
192
+ ).action(tasksCommand);
193
+
194
+ program.command('task <id>').description('One task, with the form fields needed to complete it').action(taskCommand);
195
+
196
+ program
197
+ .command('complete <taskId>')
198
+ .description('Complete a task; anything it triggers synchronously is reported here')
199
+ .option('--var <name=value>', 'repeatable; name=value, name=value:Type, or name:=<json>', collect, [])
200
+ .option('--no-wait', 'skip the follow-up check')
201
+ .action(completeCommand);
202
+
203
+ program
204
+ .command('claim <taskId>')
205
+ .description('Assign a task to yourself or someone else')
206
+ .option('-u, --user <userId>', 'defaults to the logged-in user')
207
+ .option('--unclaim', 'remove the assignee instead')
208
+ .action(claimCommand);
209
+
210
+ // --- deployment ------------------------------------------------------------
211
+ withTenant(
212
+ program
213
+ .command('deployments')
214
+ .description('Recent deployments')
215
+ .option('-n, --name <substring>')
216
+ .option('--limit <n>', 'maximum rows', int)
217
+ ).action(deploymentsCommand);
218
+
219
+ withTenant(
220
+ program
221
+ .command('deploy <files...>')
222
+ .description('Deploy BPMN or DMN files, refusing models with blocking issues')
223
+ .option('-n, --name <name>', 'deployment name')
224
+ .option('--skip-lint', 'deploy without the static checks')
225
+ ).action(deployCommand);
226
+
227
+ program
228
+ .command('undeploy <deploymentId>')
229
+ .description('Delete a deployment')
230
+ .option('--cascade', 'also delete its instances and their history')
231
+ .action(undeployCommand);
232
+
233
+ // --- events ----------------------------------------------------------------
234
+ program
235
+ .command('message <name>')
236
+ .description('Correlate a message to whatever is waiting for it')
237
+ .option('-i, --instance <processInstanceId>')
238
+ .option('-b, --business-key <key>')
239
+ .option('--all', 'correlate to every match rather than requiring exactly one')
240
+ .action(messageCommand);
241
+
242
+ program
243
+ .command('subscriptions')
244
+ .description('What is currently waiting on a message or signal')
245
+ .option('-i, --instance <processInstanceId>')
246
+ .addOption(new Option('--type <type>').choices(['message', 'signal', 'compensate', 'conditional']))
247
+ .option('--limit <n>', 'maximum rows', int)
248
+ .action(subscriptionsCommand);
249
+
250
+ // --- repair ----------------------------------------------------------------
251
+ program
252
+ .command('retry <jobId>')
253
+ .description('Give a failed job its retries back so it runs again')
254
+ .option('-r, --retries <n>', 'defaults to 1', int)
255
+ .option('--now', 'run it immediately instead of waiting for the scheduler')
256
+ .action(retryCommand);
257
+
258
+ program.command('run-job <jobId>').description('Execute a pending job right now').action(runJobCommand);
259
+
260
+ // --- identity --------------------------------------------------------------
261
+ program
262
+ .command('users')
263
+ .description('Engine-local users (these are not your business users when an identity provider is in front)')
264
+ .option('-s, --search <substring>')
265
+ .option('--limit <n>', 'maximum rows', int)
266
+ .action(usersCommand);
267
+
268
+ program.command('groups').description('Engine-local groups').option('--limit <n>', 'maximum rows', int).action(groupsCommand);
269
+
270
+ program
271
+ .command('tenants')
272
+ .description('Tenants known to the engine')
273
+ .option('-s, --search <substring>')
274
+ .option('--limit <n>', 'maximum rows', int)
275
+ .action(tenantsCommand);
276
+
277
+ program.parseAsync(process.argv).catch((err) => {
278
+ if (isJsonMode()) {
279
+ console.error(JSON.stringify({ error: err.message, status: err.status ?? null, body: err.body ?? null }, null, 2));
280
+ process.exit(1);
281
+ }
282
+ const unwrapped = unwrapError(err.body?.message || err.message);
283
+ console.error(unwrapped.message);
284
+ for (const layer of unwrapped.layers) {
285
+ const text = typeof layer.value === 'string' ? layer.value : JSON.stringify(layer.value, null, 2);
286
+ console.error(`\n${layer.label}:`);
287
+ for (const l of text.split('\n')) console.error(` ${l}`);
288
+ }
289
+ const hint = explain(err.body?.message || err.message);
290
+ if (hint) console.error(`\n${hint}`);
291
+ process.exit(1);
292
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "camunda-cli",
3
+ "version": "0.2.0",
4
+ "description": "Command-line client for self-hosted Camunda 7: inspect and lint deployed BPMN models, and diagnose why an instance is stuck",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/itsanla/camunda-cli.git"
10
+ },
11
+ "bin": {
12
+ "camunda": "bin/camunda.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "src"
20
+ ],
21
+ "keywords": [
22
+ "camunda",
23
+ "camunda7",
24
+ "bpmn",
25
+ "cli",
26
+ "self-hosted",
27
+ "workflow",
28
+ "lint",
29
+ "debugging"
30
+ ],
31
+ "dependencies": {
32
+ "commander": "^12.1.0",
33
+ "chalk": "^5.3.0",
34
+ "cli-table3": "^0.6.5"
35
+ }
36
+ }