camunda-cli 0.4.2 → 0.5.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/PANDUAN.md CHANGED
@@ -152,6 +152,20 @@ camunda cancel --key Process_X -y -r "bersih-bersih" # tanpa konfirmasi
152
152
 
153
153
  Menguji meninggalkan banyak instance menggantung. Tanpa `-y` akan diminta konfirmasi.
154
154
 
155
+ Untuk mengosongkan inbox satu penguji di AlurKerja sekaligus, tanpa peduli prosesnya:
156
+
157
+ ```bash
158
+ camunda remove dev-anla me@anla.my.id --dry-run # lihat dulu apa yang akan dihapus
159
+ camunda remove dev-anla me@anla.my.id # hapus, diminta mengetik jumlah instance
160
+ ```
161
+
162
+ Perintah ini menghapus setiap instance yang punya task terbuka milik email itu di tenant
163
+ `<Nama> (TEST)` workspace tersebut. Workspace boleh slug atau nama (`dev-anla`, `Plan Route`),
164
+ atau id tenant TEST kalau namanya ambigu (`hospital (TEST)` dan `Hospital (TEST)`) atau slug-nya
165
+ tidak mengikuti nama (`new-workspace` berjalan di `Telco (TEST)`). Id tenant LIVE ditolak.
166
+ Menghapus instance anak call activity ikut menghentikan induknya, jadi seluruh request berakhir,
167
+ termasuk task orang lain di request yang sama.
168
+
155
169
  ---
156
170
 
157
171
  ## Kasus 3: instance macet atau gagal
package/README.md CHANGED
@@ -132,7 +132,7 @@ sentence you need is at the bottom, so that is what gets printed first.
132
132
  ```
133
133
  Session login logout whoami
134
134
  Models definitions inspect lint xml stats
135
- Instances instances instance start cancel vars set-var
135
+ Instances instances instance start cancel remove vars set-var
136
136
  Diagnosis diagnose trace incidents jobs stacktrace
137
137
  Tasks tasks task complete claim
138
138
  Deployment deployments deploy undeploy
@@ -173,6 +173,12 @@ Instance 3435051 finished in 8.0s.
173
173
  **Cleaning up after a test run.** `cancel --key <key>` terminates every running instance of
174
174
  a process in one go, which matters because testing a model leaves a trail of them behind.
175
175
 
176
+ `remove <workspace> <email>` clears one tester's inbox on an AlurKerja engine instead: it
177
+ deletes every instance holding an open task assigned to that email in the workspace's
178
+ `<Name> (TEST)` tenant. The workspace is a slug or name (`dev-anla`, `Plan Route`) or a Test
179
+ tenant id; a Live tenant id is refused. `--dry-run` lists what would go. Deleting a called
180
+ child instance also terminates its parent, so the whole request ends, not only that task.
181
+
176
182
  **Variable types matter.** `--var n=300` sends a string, and `"300" > 200` is a string
177
183
  comparison. Use `--var n=300:Integer` where a gateway compares numerically, or `name:=<json>`
178
184
  for structured values.
package/bin/camunda.js CHANGED
@@ -4,7 +4,7 @@ import { configureOutput, isJsonMode } from '../src/output.js';
4
4
  import { unwrapError, explain } from '../src/errors.js';
5
5
  import { loginCommand, logoutCommand, whoamiCommand } from '../src/commands/session.js';
6
6
  import { definitionsCommand, inspectCommand, lintCommand, xmlCommand, statsCommand } from '../src/commands/definitions.js';
7
- import { instancesCommand, instanceCommand, startCommand, cancelCommand, varsCommand } from '../src/commands/instances.js';
7
+ import { instancesCommand, instanceCommand, startCommand, cancelCommand, removeCommand, varsCommand } from '../src/commands/instances.js';
8
8
  import { diagnoseCommand, incidentsCommand, jobsCommand, stacktraceCommand, traceCommand } from '../src/commands/diagnose.js';
9
9
  import { tasksCommand, taskCommand, completeCommand, claimCommand } from '../src/commands/tasks.js';
10
10
  import { deploymentsCommand, deployCommand, undeployCommand } from '../src/commands/deploy.js';
@@ -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.4.2')
27
+ .version('0.5.0')
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()
@@ -130,6 +130,13 @@ withTenant(
130
130
  .option('-y, --yes', 'skip the confirmation prompt')
131
131
  ).action(cancelCommand);
132
132
 
133
+ program
134
+ .command('remove <workspace> <assignee>')
135
+ .description("Delete every instance holding an open task for <assignee> in a workspace's TEST tenant (workspace slug, name, or TEST tenant id)")
136
+ .option('--dry-run', 'list what would be deleted and stop')
137
+ .option('-y, --yes', 'skip the confirmation prompt')
138
+ .action(removeCommand);
139
+
133
140
  program
134
141
  .command('vars <instanceId>')
135
142
  .description('Variables on an instance, or every write to them with --history')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camunda-cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Command-line client for self-hosted Camunda 7: inspect and lint deployed BPMN models, and diagnose why an instance is stuck",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.js CHANGED
@@ -159,9 +159,9 @@ export class Client {
159
159
  activityInstanceTree(id) {
160
160
  return this.get(`/process-instance/${id}/activity-instances`);
161
161
  }
162
- deleteProcessInstance(id, { reason, skipCustomListeners = true, skipIoMappings = true } = {}) {
162
+ deleteProcessInstance(id, { reason, skipCustomListeners = true, skipIoMappings = true, skipSubprocesses } = {}) {
163
163
  return this.request('DELETE', `/process-instance/${id}`, {
164
- query: { skipCustomListeners, skipIoMappings, deleteReason: reason },
164
+ query: { skipCustomListeners, skipIoMappings, skipSubprocesses, deleteReason: reason },
165
165
  });
166
166
  }
167
167
  suspendProcessInstance(id, suspended) {
@@ -187,6 +187,102 @@ export async function cancelCommand(id, options) {
187
187
  if (failures.length > 0) process.exitCode = 1;
188
188
  }
189
189
 
190
+ // AlurKerja runs each workspace on two tenants, "<Name>" for Live and "<Name> (TEST)" for
191
+ // Test. Clearing a tester's inbox only ever touches the Test one, so the workspace is
192
+ // resolved to its "(TEST)" tenant and a Live tenant id is refused outright.
193
+ const TENANT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
194
+ const TEST_SUFFIX = /\s*\(TEST\)\s*$/i;
195
+ const workspaceSlug = (name) =>
196
+ String(name ?? '')
197
+ .replace(TEST_SUFFIX, '')
198
+ .toLowerCase()
199
+ .replace(/[^a-z0-9]+/g, '-')
200
+ .replace(/^-+|-+$/g, '');
201
+
202
+ async function resolveTestTenant(client, workspace) {
203
+ if (TENANT_ID.test(workspace)) {
204
+ const [tenant] = await client.tenants({ id: workspace });
205
+ if (!tenant) throw new Error(`No tenant with id ${workspace}.`);
206
+ if (!TEST_SUFFIX.test(tenant.name ?? '')) {
207
+ throw new Error(`${workspace} is "${tenant.name}", a Live tenant. remove only clears "(TEST)" tenants.`);
208
+ }
209
+ return tenant;
210
+ }
211
+
212
+ const wanted = workspaceSlug(workspace);
213
+ const all = await client.tenants({ maxResults: 1000 });
214
+ const matches = all.filter((t) => TEST_SUFFIX.test(t.name ?? '') && workspaceSlug(t.name) === wanted);
215
+ if (matches.length === 1) return matches[0];
216
+ if (matches.length > 1) {
217
+ const list = matches.map((t) => ` ${t.id} ${t.name}`).join('\n');
218
+ throw new Error(`"${workspace}" matches ${matches.length} Test tenants. Pass the tenant id instead:\n${list}`);
219
+ }
220
+ // The engine only knows tenant names, and a workspace slug does not always follow its
221
+ // name (the "new-workspace" slug runs on "Telco (TEST)").
222
+ throw new Error(`No Test tenant named like "${workspace}". Find it with: camunda tenants -s <name>, then pass its id.`);
223
+ }
224
+
225
+ // Clears every open task assigned to one person in a workspace's Test tenant by deleting
226
+ // the instance each task belongs to. Repeated test runs fill a tester's inbox faster than
227
+ // cancelling them one request at a time in the App can empty it.
228
+ export async function removeCommand(workspace, assignee, options) {
229
+ const client = new Client(requireConfig());
230
+ const tenant = await resolveTestTenant(client, workspace);
231
+
232
+ const tasks = await client.tasks({ tenantIdIn: tenant.id, assignee });
233
+ const instanceIds = [...new Set(tasks.map((t) => t.processInstanceId).filter(Boolean))].sort();
234
+
235
+ if (out.isJsonMode() && options.dryRun) return out.json({ tenant, assignee, tasks, instanceIds });
236
+
237
+ out.heading(`${tenant.name} ${tenant.id}`);
238
+ out.kv([['assignee', assignee]]);
239
+ if (tasks.length === 0) return out.note(`\nNo open tasks assigned to ${assignee}.`);
240
+
241
+ const perName = new Map();
242
+ for (const t of tasks) perName.set(t.name ?? '(unnamed)', (perName.get(t.name ?? '(unnamed)') ?? 0) + 1);
243
+ out.line(`\n${tasks.length} open task(s) across ${instanceIds.length} instance(s)`);
244
+ out.table(
245
+ ['TASKS', 'NAME'],
246
+ [...perName].sort((a, b) => b[1] - a[1]).map(([name, count]) => [String(count), out.truncate(name, 60)])
247
+ );
248
+
249
+ if (options.dryRun) return out.note('\nDry run, nothing deleted.');
250
+
251
+ if (!options.yes) {
252
+ const rl = createInterface({ input: stdin, output: stdout });
253
+ const expect = String(instanceIds.length);
254
+ const answer = await rl.question(
255
+ `\nDeleting ${expect} instance(s) cannot be undone. Type "${expect}" to confirm: `
256
+ );
257
+ rl.close();
258
+ if (answer.trim() !== expect) return out.note('Aborted, that did not match.');
259
+ }
260
+
261
+ let removed = 0;
262
+ let gone = 0;
263
+ const failures = [];
264
+ for (const id of instanceIds) {
265
+ try {
266
+ await client.deleteProcessInstance(id, { skipSubprocesses: true });
267
+ removed++;
268
+ } catch (err) {
269
+ // Two tasks can share an instance with a parent that was deleted first.
270
+ if (err.status === 404) gone++;
271
+ else failures.push(`${id}: ${err.body?.message || err.message}`);
272
+ }
273
+ }
274
+ const left = await client.tasks({ tenantIdIn: tenant.id, assignee });
275
+
276
+ if (out.isJsonMode()) {
277
+ return out.json({ tenant, assignee, removed, alreadyGone: gone, failures, remainingTasks: left.length });
278
+ }
279
+ out.line(`\nDeleted ${removed} instance(s).${gone ? ` ${gone} were already gone.` : ''}`);
280
+ for (const f of failures) out.problem(` ${f}`);
281
+ if (left.length > 0) out.warn(`${left.length} task(s) still assigned to ${assignee}.`);
282
+ else out.note(`No open tasks left for ${assignee}.`);
283
+ if (failures.length > 0) process.exitCode = 1;
284
+ }
285
+
190
286
  export async function varsCommand(id, options) {
191
287
  const client = new Client(requireConfig());
192
288
 
@@ -65,7 +65,23 @@ export async function messageCommand(name, options) {
65
65
  if (options.businessKey) body.businessKey = options.businessKey;
66
66
  if (options.all) body.all = true;
67
67
 
68
- const result = await client.correlateMessage(body);
68
+ // Camunda answers a message that matches nothing with an exception rather than an empty
69
+ // result, so the ordinary case of getting the name or business key wrong arrives as a
70
+ // Java class name unless it is caught here.
71
+ let result;
72
+ try {
73
+ result = await client.correlateMessage(body);
74
+ } catch (err) {
75
+ const raw = err.body?.message || err.message || '';
76
+ if (/MismatchingMessageCorrelation|No process definition or execution matches/.test(raw)) {
77
+ out.warn(`Nothing is waiting for a message called "${name}"${options.instance ? ` on instance ${options.instance}` : ''}${options.businessKey ? ` with business key ${options.businessKey}` : ''}.`);
78
+ out.note('camunda subscriptions shows what is currently waiting, and under which name.');
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ throw err;
83
+ }
84
+
69
85
  if (out.isJsonMode()) return out.json(result);
70
86
 
71
87
  if (!result || result.length === 0) {